diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a207de0..cb2a696 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,6 +37,14 @@ updates: major-updates: update-types: - "major" + ignore: + # typescript-eslint declares `typescript: >=4.8.4 <6.1.0`, and TypeScript 7 + # removed `ts.Extension`, which @typescript-eslint/typescript-estree reads + # at import time. Bumping to 7 makes `eslint .` fail to load its own config, + # so `npm run lint` cannot run at all. Drop this once typescript-eslint + # supports TypeScript 7. + - dependency-name: "typescript" + update-types: ["version-update:semver-major"] # Docker base images (docker/Dockerfile + docker-compose / compose-dev) - package-ecosystem: "docker" diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 4f18967..bf771ed 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" @@ -67,7 +67,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" @@ -78,11 +78,8 @@ jobs: - name: Run ESLint run: npx eslint . - - name: Run Prettier check - run: npx prettier --check . - - name: Type check - run: npx tsc --noEmit + run: npm run type-check - name: Run unit tests run: npm run test @@ -121,7 +118,7 @@ jobs: CHANGES=$(git log --oneline --no-merges "${{ steps.prev.outputs.sha }}..${{ needs.prep.outputs.sha }}" -- . ':!package-lock.json' | sed 's/^/- /') fi if [ -z "$CHANGES" ]; then - CHANGES="- No new commits since the last beta (or this is the first beta build)." + CHANGES="- No new commits since the last beta." fi cat > BETA_RELEASE_BODY.md << EOF @@ -157,7 +154,7 @@ jobs: docker: needs: [prep, verify, create-release] - if: ${{ always() && needs.prep.outputs.dev_branch != '' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }} + if: ${{ always() && needs.prep.outputs.dev_branch != '' && needs.verify.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }} uses: ./.github/workflows/docker.yml with: version: ${{ needs.prep.outputs.beta_version }} diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml new file mode 100644 index 0000000..2a63ea1 --- /dev/null +++ b/.github/workflows/crowdin-sync.yml @@ -0,0 +1,83 @@ +name: Crowdin Sync + +on: + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + inputs: + branch: + description: "Branch to sync translations into" + required: false + type: string + +permissions: + contents: write + +jobs: + crowdin: + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - name: Resolve target branch + id: branch + run: | + BRANCH="${{ inputs.branch }}" + if [ -z "$BRANCH" ]; then + BRANCH="${{ github.event.repository.default_branch }}" + fi + echo "name=$BRANCH" >> "$GITHUB_OUTPUT" + + - name: Checkout branch + uses: actions/checkout@v7 + with: + ref: ${{ steps.branch.outputs.name }} + fetch-depth: 0 + token: ${{ secrets.GHCR_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version-file: ".nvmrc" + + - name: Upload sources to Crowdin + uses: crowdin/github-action@v2 + with: + upload_sources: true + upload_translations: false + download_translations: false + create_pull_request: false + push_translations: false + token: ${{ secrets.CROWDIN_API_KEY }} + project_id: "858252" + env: + CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} + + - name: Machine pre-translate untranslated strings + env: + CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} + run: node scripts/crowdin-pretranslate.cjs + + - name: Download translations from Crowdin + uses: crowdin/github-action@v2 + with: + upload_sources: false + upload_translations: false + download_translations: true + create_pull_request: false + push_translations: false + token: ${{ secrets.CROWDIN_API_KEY }} + project_id: "858252" + env: + CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} + + - name: Commit translations + run: | + git config user.name "LukeGus" + git config user.email "bugattiguy527@gmail.com" + + git add src/ui/locales/translated + if git diff --cached --quiet; then + echo "No translation changes to commit." + exit 0 + fi + git commit -m "chore: sync Crowdin translations" + git push origin HEAD:"${{ steps.branch.outputs.name }}" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4b5ae3f..109e7a4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -43,6 +43,7 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 20 permissions: contents: read packages: write diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index 324cc50..27b1a4f 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -72,11 +72,48 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" + - name: Install Spectre-mitigated MSVC libraries + shell: pwsh + run: | + # node-pty's binding.gyp sets SpectreMitigation, so MSBuild refuses to + # build without these. They are not on the runner image by default. + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $installPath = & $vswhere -latest -products * -property installationPath + if (-not $installPath) { throw "Visual Studio installation not found" } + + # Derive the toolset version from the installed MSVC so the component + # id keeps matching when the runner image bumps the compiler. + $toolsetDir = Get-ChildItem -Path "$installPath\VC\Tools\MSVC" -Directory | + Sort-Object Name -Descending | Select-Object -First 1 + if (-not $toolsetDir) { throw "No MSVC toolset found under $installPath" } + $parts = $toolsetDir.Name.Split(".") + $shortVer = "$($parts[0]).$($parts[1].Substring(0,2))" + Write-Host "MSVC toolset $($toolsetDir.Name) -> component version $shortVer" + + $installer = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe" + $components = @( + "Microsoft.VisualStudio.Component.VC.$shortVer.17.14.x86.x64.Spectre", + "Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre" + ) + $args = @("modify", "--installPath", "`"$installPath`"", "--quiet", "--norestart", "--nocache") + foreach ($c in $components) { $args += @("--add", $c) } + + Write-Host "Installing: $($components -join ', ')" + $proc = Start-Process -FilePath $installer -ArgumentList $args -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) { + Write-Host "vs_installer exited with $($proc.ExitCode); verifying libraries anyway" + } + + $found = Get-ChildItem -Path "$installPath\VC\Tools\MSVC" -Recurse -Filter "*.lib" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match "\\spectre\\" } | Select-Object -First 1 + if (-not $found) { throw "Spectre-mitigated libraries still missing after install" } + Write-Host "Spectre libs present: $($found.FullName)" + - name: Install dependencies run: npm ci @@ -166,7 +203,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" @@ -380,7 +417,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" @@ -559,7 +596,7 @@ jobs: CHECKSUM=$(shasum -a 256 "$DMG_PATH" | awk '{print $1}') mkdir -p homebrew-generated - cp packaging/Casks/termix.rb homebrew-generated/termix.rb + cp Casks/termix.rb homebrew-generated/termix.rb sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-generated/termix.rb sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-generated/termix.rb @@ -894,7 +931,7 @@ jobs: mkdir -p homebrew-submission/Casks/t - cp packaging/Casks/termix.rb homebrew-submission/Casks/t/termix.rb + cp Casks/termix.rb homebrew-submission/Casks/t/termix.rb sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb @@ -966,7 +1003,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" @@ -1075,13 +1112,21 @@ jobs: default_platform(:mac) lane :fetch_build_number do - number = app_store_build_number( - live: false, + live_number = app_store_build_number( + live: true, + platform: "osx", api_key_path: "/tmp/asc_keys/api_key.json", app_identifier: "com.karmaa.termix", - version: "$APP_VERSION", initial_build_number: 0, ) + pending_number = app_store_build_number( + live: false, + platform: "osx", + api_key_path: "/tmp/asc_keys/api_key.json", + app_identifier: "com.karmaa.termix", + initial_build_number: 0, + ) + number = [live_number, pending_number].max File.write("$OUT_FILE", number.to_s) end EOF @@ -1109,6 +1154,18 @@ jobs: BUILD_VERSION="${{ steps.build_number.outputs.build_version || github.run_number }}" npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" + - name: Generate App Store release notes + id: asc_notes + if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true' + run: | + META_DIR="$RUNNER_TEMP/asc_metadata" + rm -rf "$META_DIR" + node scripts/generate-appstore-notes.cjs \ + --notes RELEASE_NOTES.md \ + --out-dir "$META_DIR" \ + --locales "en-US" + echo "metadata_path=$META_DIR" >> "$GITHUB_OUTPUT" + - name: Upload and submit to Mac App Store if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true' run: | @@ -1125,10 +1182,16 @@ jobs: --pkg "$PKG_FILE" \ --api_key_path "$API_KEY_JSON" \ --app_version "$VERSION" \ - --skip_metadata true \ + --platform osx \ + --app_identifier "com.karmaa.termix" \ + --skip_metadata false \ + --metadata_path "${{ steps.asc_notes.outputs.metadata_path }}" \ --skip_screenshots true \ + --skip_app_version_update false \ --submit_for_review true \ --automatic_release true \ + --precheck_include_in_app_purchases false \ + --submission_information "{\"export_compliance_uses_encryption\": false}" \ --force true - name: Clean up keychains diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index c76952d..54f1486 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 2e48e16..727c51e 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" @@ -24,14 +24,89 @@ jobs: - name: Install dependencies run: npm ci - - name: Run ESLint - run: npx eslint . + - name: Lint + # npm run lint, not npx eslint โ€” the script also checks that the + # generated dialect schemas match schema.ts, which eslint cannot see. + run: npm run lint - name: Run Prettier check run: npx prettier --check . - name: Type check - run: npx tsc --noEmit + run: npm run type-check - name: Build run: npm run build + + database-dialects: + name: Postgres and MySQL + runs-on: blacksmith-2vcpu-ubuntu-2404 + + # The test suite only ever sees SQLite. Everything that differs per engine โ€” + # the RETURNING replacements, the read-then-write transactions, the + # migrations themselves โ€” is only covered here, against real servers. + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: termix + POSTGRES_PASSWORD: termix + POSTGRES_DB: termix_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + mysql: + image: mysql:8 + env: + MYSQL_ROOT_PASSWORD: termix + MYSQL_DATABASE: termix_test + MYSQL_USER: termix + MYSQL_PASSWORD: termix + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping -h 127.0.0.1 -ptermix" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version-file: ".nvmrc" + cache: "npm" + + - name: Install dependencies + run: npm ci + + # Each run applies the migrations to an empty database first, so a + # migration that does not apply cleanly fails the build. + - name: Verify Postgres + run: npm run verify:dialect -- postgres://termix:termix@127.0.0.1:5432/termix_test + + - name: Verify MySQL + run: npm run verify:dialect -- mysql://termix:termix@127.0.0.1:3306/termix_test + + # The same repository suite the SQLite run executes, pointed at each + # engine. This is where a dialect difference in a query shows up as a + # failing assertion rather than as a bug report. + - name: Repository tests on Postgres + env: + TEST_DIALECT: postgres + TEST_DATABASE_URL: postgres://termix:termix@127.0.0.1:5432/termix_test + run: npx vitest run src/backend/tests/database/repositories --no-file-parallelism + + - name: Repository tests on MySQL + env: + TEST_DIALECT: mysql + TEST_DATABASE_URL: mysql://termix:termix@127.0.0.1:3306/termix_test + run: npx vitest run src/backend/tests/database/repositories --no-file-parallelism diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf89c5a..f59c5a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" @@ -92,7 +92,7 @@ jobs: token: ${{ secrets.GHCR_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" cache: "npm" @@ -144,7 +144,7 @@ jobs: token: ${{ secrets.GHCR_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" @@ -225,7 +225,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" @@ -304,7 +304,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" @@ -395,10 +395,10 @@ jobs: git fetch origin main git checkout -B main origin/main - sed -i "s|version \".*\"|version \"$VERSION\"|g" packaging/Casks/termix.rb - sed -i "s|sha256 \".*\"|sha256 \"$DMG_SHA256\"|g" packaging/Casks/termix.rb + sed -i "s|version \".*\"|version \"$VERSION\"|g" Casks/termix.rb + sed -i "s|sha256 \".*\"|sha256 \"$DMG_SHA256\"|g" Casks/termix.rb - git add packaging/Casks/termix.rb + git add Casks/termix.rb if git diff --cached --quiet; then echo "Cask already up to date." exit 0 @@ -420,7 +420,7 @@ jobs: path: termix - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: "termix/.nvmrc" cache: "npm" @@ -443,13 +443,21 @@ jobs: - name: Create docs release branch working-directory: docs-repo - run: git checkout -B "dev-${{ needs.prep.outputs.version }}" + run: | + BRANCH="dev-${{ needs.prep.outputs.version }}" + if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then + echo "Reusing existing docs branch $BRANCH." + git checkout -B "$BRANCH" "origin/$BRANCH" + else + git checkout -B "$BRANCH" + fi - name: Overwrite OpenAPI spec and regenerate API docs working-directory: docs-repo run: | cp ../termix/openapi.json static/openapi.json npm ci + npm run docusaurus clean-api-docs termix npm run docusaurus gen-api-docs termix - name: Commit and push docs branch @@ -471,7 +479,7 @@ jobs: git add -A git commit -m "feat: update API docs for ${{ needs.prep.outputs.version }}" - git push --force origin "dev-${{ needs.prep.outputs.version }}" + git push origin "dev-${{ needs.prep.outputs.version }}" - name: Open and squash-merge docs PR if: ${{ inputs.mode != 'Dry run' }} @@ -513,7 +521,7 @@ jobs: fetch-depth: 1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: ".nvmrc" @@ -534,7 +542,7 @@ jobs: docs, publish-youtube, ] - if: ${{ always() && (inputs.mode == 'Everything' || inputs.mode == 'Skip submit') && needs.merge-to-main.result == 'success' && needs.electron-release.result == 'success' }} + if: ${{ always() && (inputs.mode == 'Everything' || inputs.mode == 'Skip submit') && needs.merge-to-main.result == 'success' && needs.docs.result == 'success' }} runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index d9251b2..cfb1233 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,8 @@ dist-ssr coverage *.local -.vscode/ +.vscode/* +!.vscode/settings.json .idea .DS_Store *.suo @@ -33,5 +34,7 @@ electron/build-info.cjs /.mcp.json /CLAUDE.md /old_db/ -/scripts/fix-bugs.mjs -/scripts/fix-features.mjs +/scripts/auto-fix.mjs +/scripts/auto-fix-blocklist.json +/scripts/auto-fix-state.json +/scripts/auto-fix-report-*.json diff --git a/.husky/commit-msg b/.husky/commit-msg index 0a4b97d..da99483 100644 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1 +1 @@ -npx --no -- commitlint --edit $1 +npx --no -- commitlint --edit "$1" diff --git a/.prettierignore b/.prettierignore index befe2e1..8cf32b0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,3 +17,6 @@ db *.min.js *.min.css openapi.json + +# Generated by drizzle-kit; formatting is the tool's own +drizzle/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dff3ab4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.prettierPath": "./node_modules/prettier", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false + }, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/packaging/Casks/termix.rb b/Casks/termix.rb similarity index 87% rename from packaging/Casks/termix.rb rename to Casks/termix.rb index 9e42e20..7458d04 100644 --- a/packaging/Casks/termix.rb +++ b/Casks/termix.rb @@ -1,6 +1,6 @@ cask "termix" do - version "2.5.1" - sha256 "39f88b6fb6f8841496fe689968decbbc4f4baa92ab5a3a41a738123cf7daeb3f" + version "2.7.0" + sha256 "8cdae5cf5ce2786e35a1a676dec51974cfca318a5595bc743981a4c373515059" url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg" name "Termix" diff --git a/README.md b/README.md index 6ec9996..95c175b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Termix

-

Self-hosted SSH management and remote desktop access

+

Self-hosted server management, from SSH and remote desktop to automations

English ยท @@ -58,7 +58,7 @@ Termix is free and open source. If you find it useful, consider [donating](https ## Overview -Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal access, remote desktop control (RDP, VNC, Telnet), SSH tunneling capabilities, remote file management, and many other tools. Termix is the perfect free and self-hosted alternative to Termius available for all platforms. +Termix is a free, open source, self-hosted platform for managing your servers. It puts SSH terminals, remote desktops (RDP, VNC, Telnet), file transfers, tunnels, Docker, metrics, and automations in one place, on web, desktop, and mobile. It is a self-hosted alternative to Termius that stays free forever.
@@ -68,42 +68,42 @@ Termix is an open-source, forever-free, self-hosted all-in-one server management -**SSH Terminal Access:** -Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components. +**SSH Terminal:** +A full terminal with browser-like tabs and split screen, up to 6 panels at once. Pick your theme, font, and colors. A toolbar sits above each session with live CPU, memory, and disk, plus quick links to that host's files, Docker, tunnels, and metrics. -**Remote Desktop Access:** -RDP, VNC, and Telnet support over the browser with complete customization and split screening. +**Remote Desktop:** +RDP, VNC, and Telnet in the browser, in tabs and split screen like any other session. Includes a file browser for RDP drives and drag-and-drop upload. On Windows desktop you can also open a host in the native RDP client. -**SSH Tunnel Management:** -Create and manage server-to-server SSH tunnels with automatic reconnection, health monitoring, and local, remote, or dynamic SOCKS forwarding. Desktop client-to-server tunnel settings are stored locally per desktop install, optional C2S preset snapshots can be saved to the server, renamed, loaded, or deleted when you want to move a local tunnel configuration between clients. +**SSH Tunnels:** +Local, remote, and dynamic SOCKS forwarding with auto reconnect and health checks. Client-to-server tunnels on the desktop app are stored on that machine, and you can save presets to the server to move a setup to another client. -**Remote File Manager:** -Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly with sudo support. Includes support for moving files from server to server. +**File Manager:** +Browse, edit, upload, download, rename, move, and delete files over SFTP, with sudo support. View and edit code, images, audio, and video. Copy files straight from one server to another, with the fastest route picked for you and transfers checked for integrity. -**Docker and Podman Management:** -Start, stop, pause, remove containers. View container stats. Control containers using a docker exec terminal. Supports both Docker and Podman as the container runtime. It was not made to replace Portainer or Dockge but rather to simply manage your containers compared to creating them. +**Docker and Podman:** +Start, stop, pause, and remove containers, watch their stats, and open a shell inside one. Works with both Docker and Podman. It is not meant to replace Portainer or Dockge, just to manage containers you already have. -**SSH Host Manager:** -Save, organize, and manage your SSH connections with tags and folders (folder customization and nested folder support), and easily save reusable login info while being able to automate the deployment of SSH keys. +**Host Manager:** +Save and organize hosts with tags and nested folders you can name and color. Reuse saved credentials across hosts, deploy SSH keys automatically, group hosts under a parent host, bulk edit and export, and use Quick Connect for one-off connections you do not want to save. @@ -111,83 +111,139 @@ Save, organize, and manage your SSH connections with tags and folders (folder cu **Host Metrics:** -View CPU, memory, disk usage, network, uptime, system information, firewall, port monitor, log viewer, users/permissions, certificates, and many more which work on most Linux based servers. Includes time-series history graphs and threshold-based alerts with ntfy and webhook support. +CPU, memory, disk, network, temperature, uptime, processes, ports, logins, and system info on most Linux servers, with history graphs. Manager cards let you handle services, cron jobs, packages, users, firewall rules, WireGuard, Tailscale, SSL certs, logs, and health checks without leaving Termix. -**User Authentication:** -Secure user management with admin controls (can edit other users information) and OIDC/LDAP/SSO (with access control), 2FA (TOTP), and passkey (WebAuthn) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together. View audit log of all users actions. +**Automations:** +Pick a trigger, then say what should happen. Triggers include a metric crossing a threshold, a host going up or down, a health check changing, a schedule, a container event, or an incoming webhook. Steps can run commands and snippets, control containers and tunnels, wake a host, call a URL, wait, branch on a condition, run another automation, and notify you over ntfy, Discord, or a webhook. Test runs let you try it safely first. -**Tailscale Integration:** -List devices from your tailnet to quickly add them as hosts, and connect using Tailscale SSH as an authentication method, letting your tailnet ACLs handle authorization without storing credentials. +**Fleets:** +Group hosts into a fleet by picking them or with tag rules, so new hosts join on their own. Run one command on every host at once, push and pull files across all of them, install packages, and collect an inventory of OS, kernel, arch, and uptime. -**RBAC/Sharing:** -Create roles and share hosts across users/roles. Supports all auth types and all host protocols. +**AI Assistant:** +Optional, and off until you turn it on. Connect OpenAI, Anthropic, Gemini, Ollama, or any OpenAI compatible endpoint and ask about your setup. It reads hosts, fleets, snippets, and alerts, and proposes changes for you to approve instead of making them. It can never touch credentials, users, or settings. Admins can leave it off for the whole instance, and you can hide it during setup. -**Serial Connections:** -Connect to serial devices (routers, switches, microcontrollers, etc.) directly from the browser or desktop app. Configure baud rate, data bits, stop bits, and parity. Uses the Web Serial API in supported browsers or a native backend in the Electron app. +**Login and Users:** +Local accounts plus OIDC, LDAP, GitHub, and Google sign-in, with 2FA (TOTP), passkeys (WebAuthn), and trusted devices. Admins can manage users, map OIDC groups to roles, see every active session across platforms, and revoke them. Link your local and OIDC accounts together, and read the audit log of what everyone did. +**Roles and Sharing:** +Create roles and share hosts with users or roles at four levels: connect, view, edit, and manage. Works with every auth type and every protocol, and you can override the credentials used for a shared host. + + + + + + **Alerts:** -Set threshold-based alert rules on host metrics (CPU, memory, disk, etc.) and get notified via ntfy or webhooks when they fire. View firing and resolved alerts in a history log. +Set rules on host metrics like CPU, memory, and disk, and get notified over ntfy, Discord, or a webhook when they fire. See firing and resolved alerts in a history log, and dismiss the ones you do not care about. - - **Homepage:** -A fully customizable homepage with a drag-and-drop widget grid. Add widgets for host status, service links, clocks, notes, RSS feeds, weather, Docker containers, host metrics charts, embedded terminals, iframes, and more. - - - - -**Database Encryption:** -Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more. +A drag-and-drop widget grid you build yourself. Widgets for host status, pings, service links, bookmarks, search, clocks, calendars, countdowns, notes, RSS, weather, images, iframes, Docker, tunnels, metrics charts, custom APIs, and even a live terminal. -**Network Graph:** -Customize your Dashboard to visualize your homelab based off your SSH connections with status support. +**Snippets and Tools:** +Save commands you run often and fire them off in one click, with variables for the host and your own inputs. Run a single command across every open terminal, and search your command history with autocomplete. -**SSH Tools:** -Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals. +**Session Sharing:** +Share a live terminal, RDP, VNC, or Telnet session in real time. Send a link anyone can join without an account, or share with a specific Termix user, in read-only or read-write mode. Shares can expire on their own or be revoked, and can be turned off globally or per host. -**Persistent Tabs:** -SSH sessions and tabs stay open across devices/refreshes if enabled in user profile. +**Session Recording and Logs:** +Record terminal, RDP, and VNC sessions and play them back later. Download plain text logs of a session, and check the connection log to see exactly what happened during a connection. + + + + +**Serial Connections:** +Talk to serial devices like routers, switches, and microcontrollers from the browser or desktop app. Set baud rate, data bits, stop bits, and parity. Uses the Web Serial API in supported browsers, or a native backend in the desktop app. + + + + + + +**Tailscale:** +Pull devices from your tailnet to add them as hosts in a couple of clicks, and connect with Tailscale SSH so your tailnet ACLs handle access and no credentials are stored. Headscale and custom endpoints work too. + + + + +**Proxmox:** +Import hosts straight from a Proxmox instance, and watch node and guest stats, including CPU, memory, and storage, in their own tab. + + + + + + +**Workspaces and Tabs:** +Save a set of tabs with their split layout and reopen the whole thing in one click. Termix also remembers your last session, so your tabs come back across refreshes and devices. + + + + +**Guided Setup:** +A short setup walks you through picking an interface preset, your theme, the features you want, and your first host. Simple mode hides what you do not use, and you can rerun setup or switch presets any time. + + + + + + +**Desktop Standalone and Sync:** +The desktop app runs on its own with a local backend and database, no server needed. You can also connect it to a Termix server for two-way sync of hosts, credentials, snippets, and more, and choose whether connections start locally or through the server. + + + + +**Command Line Interface:** +A `termix` CLI for your shell and your scripts. Open terminals, run a command on one host or a whole fleet, move files over SFTP, and manage hosts, snippets, and credentials. Install with `npm install -g @termix-cli/cli` or grab a standalone binary. See the [CLI docs](https://docs.termix.site/cli). + + + + + + +**Security:** +Passwords, keys, and other secrets are encrypted per user, and the database files themselves can be encrypted on disk. See the [docs](https://docs.termix.site/security) for how it works. **Languages:** -Built-in support ~30 languages (managed by [Crowdin](https://docs.termix.site/translations)). +Around 30 languages built in, managed through [Crowdin](https://docs.termix.site/translations). @@ -199,17 +255,20 @@ Built-in support ~30 languages (managed by [Crowdin](https://docs.termix.site/tr

More features
-- **Dashboard** - View server information at a glance on your dashboard -- **API Keys** - Create user-scoped API keys with expiration dates to be used for automation/CI -- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data -- **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects -- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between many different UI themes including light, dark, Dracula, etc. Use URL routes to open any connection in full-screen. -- **Command History** - Auto-complete and view previously ran SSH commands -- **Quick Connect** - Connect to a server without having to save the connection data -- **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard -- **Proxmox Integration** - Auto-add hosts into Termix from your Proxmox instance -- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, terminal logging, SSH agent forwarding, Bitwarden SSH agent, HashiCorp Vault SSH signing, and more. -- **Termix ID** - A sshid.io equivalent built into Termix. Claim a handle, publish your public SSH keys at a resolver URL, and use a built-in CA to issue SSH certificates. +- **Dashboard** - Your servers at a glance, with cards you arrange yourself +- **Network Graph** - See your homelab drawn out from your hosts, with live status +- **Tmux Monitor** - Browse tmux sessions, windows, and panes, with previews and search +- **API Keys** - User-scoped keys with expiry dates for scripts and CI +- **Export and Import** - Move hosts, credentials, and file manager data in and out +- **Automatic SSL** - Certificates generated and renewed for you, with HTTPS redirects, or bring your own +- **Databases** - SQLite by default, with PostgreSQL and MySQL supported too +- **Modern UI** - Clean React interface that works on desktop and mobile, with themes like light, dark, and Dracula. Any connection can open full screen from a URL +- **Command Palette** - Double tap left shift to jump to a host from the keyboard +- **Keyboard Shortcuts** - Move between tabs, close tabs, and more, all rebindable +- **Wake-on-LAN** - Wake a machine from Termix or from an automation step +- **Trusted Proxy Auth** - Let a reverse proxy handle sign-in and pass the user through +- **SSH Feature Rich** - Jump hosts, Warpgate, TOTP prompts, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, terminal logging, agent forwarding, Bitwarden SSH agent, HashiCorp Vault SSH signing, and more +- **Termix ID** - A built-in take on sshid.io. Claim a handle, publish your public keys at a resolver URL, and issue SSH certificates from the built-in CA @@ -291,6 +350,32 @@ networks: driver: bridge ``` +### Command Line Interface + +Termix also has a CLI, so you can manage your servers from a terminal and use Termix in your own scripts. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +It can open terminals, run a command on one host or a whole fleet, move files over SFTP, and manage hosts, snippets and credentials. Full documentation is at [docs.termix.site/cli](https://docs.termix.site/cli). + +### Cloud Hosting + +You can run the Termix server on a VPS instead of inside your own network. If Termix runs on the network it manages, an outage takes Termix down with it, right when you need it to fix things. Running it elsewhere keeps it reachable, gives you a static IP, and lets you get in from anywhere without a VPN or port forward. + +[GINERNET](https://docs.termix.site/install/ginernet) sponsors Termix, and the docs have a step by step guide for deploying to their VPS platform. + +
+ +## Telemetry + +Termix sends a small anonymous ping once a day so I can see how many instances are running and which features get used. It contains a random instance ID, how many users and hosts you have, the app version, and which features (terminal, file manager, tunnels, docker, etc.) were used in the last 24 hours. It never contains usernames, hostnames, IP addresses, credentials, or anything else that identifies you or your servers. + +It is on by default. Turn it off in Admin Settings under General, or set `ENABLE_TELEMETRY=false` before you ever start Termix. +
## Donate @@ -325,10 +410,6 @@ Interested in a paid placement to support development? Email [mail@termix.site]( Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Interested in a paid placement to support development? Email [mail@termix.site]( Rack Genius - +    + + Ginernet +
## Support -If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`. Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel, however, response times may be longer. +Need help or want to request a feature? Open a [new issue](https://github.com/Termix-SSH/Support/issues) and add as much detail as you can, in English if possible. You can also ask in the support channel on [Discord](https://discord.gg/jVQGdvHDrf), though replies there can take longer.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e4ab722..6623010 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,70 +1,125 @@ -Revamped RBAC/sharing, session recording & replay, Vault auth for monitors, API key host enrollment, Proxmox guest auto sync, database refactor, plus 30+ bug fixes across terminal, file manager, RDP/VNC, and auth. DO NOT DOWNGRADE FROM THIS VERSION. +Termix AI, automations, fleets, workspaces, subhosts, Proxmox metrics, a context aware terminal toolbar, split screen tabs, onboarding, PostgreSQL/MySQL support, and a large batch of fixes. -https://youtu.be/c3UD4q2jW_8 +https://youtu.be/lngaePO96tM -- Revamped RBAC/sharing system (new UI, all auth types and host protocols now supported) -- Complete admin control over user information (manage all users hosts, credentials, and snippets) -- Support Vault auth for monitors -- API key host enrollment endpoint -- Allow pinned hosts with name sorting -- Session recording and replay -- Terminal font size shortcuts (ctrl + / -) -- Open File Manager to tab right-click menu -- Proxmox guest auto sync -- Complete database refactor -- 30-day donation reminder and new donation milestones that support research: (donate.termix.site) -- Improve site performance with cache and poll pauses -- Save quick connect sessions as hosts +- Added completely optional and disabled/removed by default Termix AI, an assistant that can work with your hosts and terminals + - This feature was added based off a 60% (yes) to 40% (no) Discord vote. + - The assistant cannot change anything on its own. It can only read a limited set of your Termix data and propose actions, and every change or command runs only after you approve it, using your own account and permissions. + - It has no access to credentials, SSH keys, vaults, users, roles, sessions, SSO, certificates, audit logs, or instance settings. Secrets are also stripped from anything sent to a model provider. + - Both an admin and each user must turn it on before it does anything, and it stays off after upgrading. +- Added automations with events, channels, and steps +- Added a fleet system with snippets, packages, files, and inventory +- Added workspaces to save and restore your tab layout +- Added subhosts so hosts can be organized under a parent host +- Added Proxmox metrics integration +- Added a context aware terminal toolbar with quick links, host info, image pasting, and a movable desktop layout +- Added interactive terminal macros +- Added first-class split screen tabs +- Added a file manager trash instead of permanent deletes +- Added a local terminal to the desktop app +- Added inheritable connection defaults so hosts can share settings +- Added an onboarding flow with an interface simplicity system +- Added PostgreSQL and MySQL support alongside SQLite +- Added a redesigned host and credential sidebar with synced preferences and drag-to-reorder +- Added the option to open some app rail tabs as their own tab or in a right sidebar +- Added folder select to host multi select +- Added connection logs for RDP, VNC, and Telnet hosts +- Added native RDP launching on Windows desktop +- Added a drive file browser and drag-and-drop upload for RDP +- Added terminal image handoff so images open on your local machine +- Added custom terminal font selection +- Added trusted proxy authentication +- Added global touch input settings +- Added adaptive transfers that pick the fastest route and verify integrity +- Added adaptive polling and preloading that respond to activity and network cost +- Added adaptive SSH local echo for high latency connections +- Added custom disk and network metric options +- Added the ability to exclude specific mounts from disk usage metrics +- Added expanded snippet options +- Added downloadable session logs as text files +- Added keyboard shortcuts to move between open tabs +- Added Discord webhook notification channels +- Added paste support when not running over HTTPS +- Added Headscale API key and custom API endpoint support +- Added a Meta key option for terminals +- Added BE-AZERTY keyboard layout for remote desktop +- Added PKCE to the OIDC login flow +- Added Proxmox VMID and Docker tags to discovered guests +- Added custom SSL certificate support in admin settings +- Greatly improved performance across metrics polling and host management for large setups + -- Syntax highlighting artifacts -- Filter dashboard status hosts -- Persist dashboard service link changes -- Snippet text overflow -- Persist remote desktop credential auth -- Guard language switching failures -- Resolve tunnel source credentials -- Windows file delete command -- Artifact release checkout ref -- Command palette escape in fullscreen -- Alerts and audit log normalization -- macOS VNC protocol negotiation -- Port knocking before SSH connect -- Allow escape to close link confirmation -- Prevent Electron modifier wheel zoom -- Credential auth optional password -- Retry transient terminal DNS lookups -- OIDC redirect forwarded port handling -- Preserve recent open tabs on startup -- Terminal font selection -- Poor font legibility in multiple places -- File manager uploads failing -- Tmux detection for non-POSTIX shells -- OPKSSH js-yaml ESM import -- Android Vietnamese IME input -- Firefox RDP clipboard paste -- Proxmox discovery over HTTPS -- External editor actions in file preview -- Firefox desktop OIDC callback -- Status checks through jump hosts -- Restore sudo password auto fill settings -- Preserve file editor position on save -- Sync cloud preference storage mode -- Render RDP sessions at native pixel density -- Restore database import in embedded desktop mode -- Command autocomplete dropdown poor contrast -- Allow clipboard paste in key recording field -- Fix GitHub/google SSO "not defined" errors +- Periodic SSH terminal stalls caused by SQLite telemetry writes +- Missing OPKSSH binary breaking installs without internet access +- Session recording writes slowing down terminals +- SGR mouse tracking escape codes printing as text +- Terminal display distortion with special characters +- Windows Ctrl+W not closing the active tab +- Tray Quit not terminating the desktop app +- Mobile terminal scrollback not matching xterm wheel behavior +- tmux breaking on UTF-8 paths +- Sudo password auto-fill not persisting +- SSH and sudo passwords not being saved or auto-filled +- Switching SSH authentication away from Vault failing +- Host edits being discarded without a warning +- Quick-created credentials not being selected +- Saved RDP connection settings not being preserved +- RDP domain credentials not being prompted for +- Windows key mapping in remote desktop sessions +- VNC failing to connect to macOS screen sharing +- Mouse input breaking on touch-capable devices in RDP and VNC +- Docker runtime selection not persisting, plus Docker manager UI issues +- Desktop Docker console WebSocket not being authenticated +- Folders intermittently disappearing from duplicate requests +- Folder deletion not refreshing the host list +- Proxmox guest identity being lost on edit +- Long host names shifting dashboard metrics +- Host list rows resizing unexpectedly +- Metrics collection all firing at once on startup +- Session activity writes hitting the database too often +- Reachable and available hosts being treated the same +- SSH keepalives could not be disabled +- OIDC group claims from multiple sources not being merged +- OIDC discovery issuers with trailing slashes failing +- LDAP logins not using preferred_username +- Trusted MFA devices not being bound to a specific client install +- 2FA could not be disabled with a single credential +- Profile API keys not being shown after creation +- SSH agent authentication being unclear in the host editor +- Tunnel status stream not requiring authentication +- SFTP and Docker console accepting a mismatched host id +- SSH connections whose host id resolved elsewhere being accepted +- User-managed CA certificates not being applied over SFTP +- Already-shared hosts losing their SSH authentication +- Real client IP not being captured for SSH login alerts behind a reverse proxy +- Audit log IPs not using the real client IP +- Homepage System Overview update indicator never firing +- Webhook notification channels not working +- Remote sync failing behind an nginx proxy +- First server sync not refreshing the UI +- Desktop app not showing update prompts and hiding the version badge +- Desktop Tailscale configuration being lost +- Command palette not loading new activity, plus Enter now opens the first result +- Database connection failures during login not being reported clearly +- audit_logs.user_id not being nullable on fresh SQLite installs +- Sync upserts writing to the wrong row +- Database migration failures on tables without an id column +- ssh_credentials rebuilds not matching the live schema +- Sidebar reset and fullscreen buttons sharing the same icon +- Host list icons not matching the tab bar icons +- Keep Linux credential storage working on unrecognized desktops + diff --git a/biome.json b/biome.json deleted file mode 100644 index 68bf468..0000000 --- a/biome.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true, - "defaultBranch": "dev-2.5.0" - }, - "files": { - "ignoreUnknown": true, - "includes": [ - "**", - "!!build", - "!!coverage", - "!!dist", - "!!dist-ssr", - "!!release", - "!!node_modules", - "!!src/mcp-server/node_modules", - "!!db", - "!!.env", - "!!**/*.min.js", - "!!**/*.min.css", - "!!openapi.json" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 80, - "lineEnding": "lf" - }, - "linter": { - "enabled": false - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "always", - "trailingCommas": "all", - "arrowParentheses": "always" - } - }, - "json": { - "formatter": { - "trailingCommas": "none" - } - }, - "css": { - "parser": { - "tailwindDirectives": true - } - }, - "assist": { - "enabled": false - } -} diff --git a/docker/Dockerfile b/docker/Dockerfile index f6ba5f5..8507124 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Install dependencies -FROM node:26-slim AS deps +FROM node:24-slim AS deps WORKDIR /app RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* @@ -35,8 +35,26 @@ RUN npm rebuild better-sqlite3 RUN npm run build:backend -# Stage 4: Production dependencies only -FROM node:26-slim AS production-deps +# Stage 4: Download OPKSSH binary for the target platform so the image works offline +FROM node:24-slim AS opkssh-downloader +ARG TARGETARCH +ARG OPKSSH_VERSION=v0.16.0 +WORKDIR /opkssh + +RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* + +RUN case "$TARGETARCH" in \ + amd64) OPKSSH_ARCH=amd64 ;; \ + arm64) OPKSSH_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \ + esac && \ + curl -fSL -o "opkssh-linux-${OPKSSH_ARCH}" \ + "https://github.com/openpubkey/opkssh/releases/download/${OPKSSH_VERSION}/opkssh-linux-${OPKSSH_ARCH}" && \ + chmod 755 "opkssh-linux-${OPKSSH_ARCH}" && \ + echo -n "$OPKSSH_VERSION" > version.txt + +# Stage 5: Production dependencies only +FROM node:24-slim AS production-deps WORKDIR /app RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* @@ -52,13 +70,14 @@ RUN npm ci --omit=dev --ignore-scripts && \ npm rebuild better-sqlite3 bcryptjs ssh2 && \ npm cache clean --force -# Stage 5: Final optimized image -FROM node:26-slim +# Stage 6: Final optimized image +FROM node:24-slim WORKDIR /app ENV DATA_DIR=/app/data \ PORT=8080 \ - NODE_ENV=production + NODE_ENV=production \ + POSTHOG_API_KEY=phc_xM8UznirsFxUkGE68gH4jzeqevf4kh76wGw7Ci7hH2dd RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu wget certbot python3-certbot-dns-cloudflare && \ update-ca-certificates && \ @@ -74,7 +93,11 @@ COPY --chown=node:node --from=frontend-builder /app/dist /app/html COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend +COPY --chown=node:node --from=opkssh-downloader /opkssh /app/opkssh-bundled COPY --chown=node:node package.json ./ +# Schema for Postgres and MySQL. Unused by the default SQLite deployment, which +# builds its tables at startup instead. +COPY --chown=node:node drizzle ./drizzle VOLUME ["/app/data"] diff --git a/docker/compose-dev.yml b/docker/compose-dev.yml index 14b703a..3ed7795 100644 --- a/docker/compose-dev.yml +++ b/docker/compose-dev.yml @@ -13,6 +13,7 @@ services: PORT: "8080" NODE_ENV: development GUACD_HOST: "guacd-dev" + GUACD_TUNNEL_HOST: "termix-dev" GUACD_RECORDING_PATH: "/termix-data/session_recordings/guacamole" depends_on: - guacd-dev diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index eed4d17..033c6d8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -10,7 +10,15 @@ services: environment: PORT: "8080" GUACD_HOST: "guacd" + GUACD_TUNNEL_HOST: "termix" GUACD_RECORDING_PATH: "/termix-data/session_recordings/guacamole" + # Trusted reverse-proxy authentication is disabled by default. When + # enabled, do not expose this container directly to untrusted clients. + # TRUSTED_PROXY_AUTH_ENABLED: "true" + # TRUSTED_PROXY_AUTH_TRUSTED_PROXIES: "172.16.0.0/12" + # TRUSTED_PROXY_AUTH_ROLE_MAP: '{"operators":["user"]}' + # TRUSTED_PROXY_AUTH_USERNAME_HEADER: "x-forwarded-username" + # TRUSTED_PROXY_AUTH_ROLE_HEADER: "x-forwarded-role" depends_on: - guacd networks: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 917ea60..02480b7 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -22,6 +22,14 @@ if [ "$(id -u)" = "0" ]; then fi fi +DATA_DIR=${DATA_DIR:-/app/data} +if [ -f "$DATA_DIR/.env" ]; then + echo "Loading persisted SSL settings from $DATA_DIR/.env" + set -a + . "$DATA_DIR/.env" + set +a +fi + export PORT=${PORT:-8080} export ENABLE_SSL=${ENABLE_SSL:-false} export SSL_PORT=${SSL_PORT:-8443} @@ -60,8 +68,8 @@ fi OPKSSH_DIR="${DATA_DIR:-/app/data}/opkssh" if [ ! -d "$OPKSSH_DIR" ]; then - echo "WARNING: OPKSSH binary directory not found at $OPKSSH_DIR" - echo "OPKSSH will be downloaded automatically on first use." + echo "OPKSSH binary directory not found at $OPKSSH_DIR" + echo "OPKSSH will be installed from the bundled copy on first use (falls back to downloading if unavailable)." else echo "OPKSSH binary directory found at $OPKSSH_DIR" fi @@ -163,8 +171,4 @@ else echo "Warning: package.json not found" fi -node dist/backend/backend/starter.js - -echo "All services started" - -tail -f /dev/null +exec node dist/backend/backend/starter.js diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 9449028..6245ce5 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -11,6 +11,8 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; + server_tokens off; + access_log /tmp/nginx/access.log; client_body_temp_path /tmp/nginx/client_body; @@ -21,6 +23,22 @@ http { sendfile on; keepalive_timeout 65; + + # Static assets only. API responses arrive already gzipped from the node + # backend, and gzip_proxied would otherwise have nginx decompress and + # recompress them for nothing. + gzip on; + gzip_vary on; + gzip_min_length 2048; + gzip_comp_level 5; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/wasm + image/svg+xml; client_header_timeout 300s; set_real_ip_from 127.0.0.1; @@ -61,6 +79,7 @@ http { server { listen ${SSL_PORT} ssl; server_name _; + client_max_body_size 50m; absolute_redirect off; @@ -69,7 +88,6 @@ http { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; location ^~ /.well-known/acme-challenge/ { root /app/data/acme-webroot; @@ -80,6 +98,8 @@ http { location = /sw.js { root /app/html; expires off; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri =404; } @@ -87,31 +107,64 @@ http { location = /manifest.json { root /app/html; expires off; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri =404; } - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + location ^~ /assets/ { root /app/html; expires 1y; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } + location ^~ /fonts/ { + root /app/html; + expires 1y; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "public, max-age=31536000, immutable" always; + try_files $uri =404; + } + + location ^~ /icons/ { + root /app/html; + expires 30d; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "public, max-age=2592000" always; + try_files $uri =404; + } + + location ~* ^/[^/]+\.(js|css|png|jpe?g|gif|ico|svg|webp|woff2?|ttf|eot)$ { + root /app/html; + expires 30d; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "public, max-age=2592000" always; + try_files $uri =404; + } + + location ~* \.map$ { + access_log off; + log_not_found off; + return 404; + } + location / { root /app/html; index index.html index.htm; expires off; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri $uri/ /index.html; } - location ~* \.map$ { - return 404; - access_log off; - log_not_found off; - } - location ~ ^/users/sessions(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -159,6 +212,30 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/ai(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + + # The chat endpoint streams server-sent events; buffering would + # hold tokens back until the whole reply finished. + proxy_read_timeout 600s; + proxy_buffering off; + proxy_cache off; + } + + location ~ ^/automations(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/alert-rules(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -217,6 +294,21 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/fleets(/.*)?$ { + client_max_body_size 200m; + + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + location ~ ^/vault(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -226,6 +318,15 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/sync(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/termix-id(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -283,6 +384,15 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/workspaces(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/user-preferences(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -292,6 +402,33 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/host-sidebar(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + + location ~ ^/credential-sidebar(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + + location ~ ^/ui-preferences(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/database(/.*)?$ { client_max_body_size 5G; client_body_timeout 300s; @@ -363,7 +500,9 @@ http { proxy_cache_bypass 1; proxy_no_cache 1; - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; } location ~ ^/host/opkssh-callback(/.*)?$ { @@ -378,7 +517,9 @@ http { proxy_cache_bypass 1; proxy_no_cache 1; - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; } location /host/ { @@ -467,6 +608,15 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/session-sharing(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location /host/tunnel/ { proxy_pass http://127.0.0.1:30003; proxy_http_version 1.1; @@ -531,6 +681,8 @@ http { client_max_body_size 5G; client_body_timeout 300s; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate" always; proxy_pass http://127.0.0.1:30004; @@ -552,6 +704,8 @@ http { client_max_body_size 5G; client_body_timeout 300s; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate" always; proxy_pass http://127.0.0.1:30004; @@ -631,6 +785,19 @@ http { proxy_read_timeout 600s; } + location ~ ^/proxmox-stats(/.*)?$ { + proxy_pass http://127.0.0.1:30005; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + location ~ ^/global-settings(/.*)?$ { proxy_pass http://127.0.0.1:30005; proxy_http_version 1.1; @@ -753,6 +920,7 @@ http { error_page 500 502 503 504 /50x.html; location = /50x.html { root /app/html; + internal; } } } diff --git a/docker/nginx.conf b/docker/nginx.conf index 68cff5a..9140ac3 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -11,6 +11,8 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; + server_tokens off; + access_log /tmp/nginx/access.log; client_body_temp_path /tmp/nginx/client_body; @@ -21,6 +23,22 @@ http { sendfile on; keepalive_timeout 65; + + # Static assets only. API responses arrive already gzipped from the node + # backend, and gzip_proxied would otherwise have nginx decompress and + # recompress them for nothing. + gzip on; + gzip_vary on; + gzip_min_length 2048; + gzip_comp_level 5; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/wasm + image/svg+xml; client_header_timeout 300s; set_real_ip_from 127.0.0.1; @@ -54,11 +72,11 @@ http { server { listen ${PORT}; server_name _; + client_max_body_size 50m; absolute_redirect off; add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; location ^~ /.well-known/acme-challenge/ { root /app/data/acme-webroot; @@ -69,6 +87,7 @@ http { location = /sw.js { root /app/html; expires off; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri =404; } @@ -76,31 +95,58 @@ http { location = /manifest.json { root /app/html; expires off; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri =404; } - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + location ^~ /assets/ { root /app/html; expires 1y; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } + location ^~ /fonts/ { + root /app/html; + expires 1y; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "public, max-age=31536000, immutable" always; + try_files $uri =404; + } + + location ^~ /icons/ { + root /app/html; + expires 30d; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "public, max-age=2592000" always; + try_files $uri =404; + } + + location ~* ^/[^/]+\.(js|css|png|jpe?g|gif|ico|svg|webp|woff2?|ttf|eot)$ { + root /app/html; + expires 30d; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "public, max-age=2592000" always; + try_files $uri =404; + } + + location ~* \.map$ { + access_log off; + log_not_found off; + return 404; + } + location / { root /app/html; index index.html index.htm; expires off; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri $uri/ /index.html; } - location ~* \.map$ { - return 404; - access_log off; - log_not_found off; - } - location ~ ^/users/sessions(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -148,6 +194,30 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/ai(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + + # The chat endpoint streams server-sent events; buffering would + # hold tokens back until the whole reply finished. + proxy_read_timeout 600s; + proxy_buffering off; + proxy_cache off; + } + + location ~ ^/automations(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/alert-rules(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -206,6 +276,21 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/fleets(/.*)?$ { + client_max_body_size 200m; + + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + location ~ ^/vault(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -215,6 +300,15 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/sync(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/termix-id(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -272,6 +366,15 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/workspaces(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/user-preferences(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -281,6 +384,33 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/host-sidebar(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + + location ~ ^/credential-sidebar(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + + location ~ ^/ui-preferences(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location ~ ^/database(/.*)?$ { client_max_body_size 5G; client_body_timeout 300s; @@ -352,7 +482,8 @@ http { proxy_cache_bypass 1; proxy_no_cache 1; - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; } location ~ ^/host/opkssh-callback(/.*)?$ { @@ -367,7 +498,8 @@ http { proxy_cache_bypass 1; proxy_no_cache 1; - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; } location /host/ { @@ -456,6 +588,15 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + location ~ ^/session-sharing(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + } + location /host/tunnel/ { proxy_pass http://127.0.0.1:30003; proxy_http_version 1.1; @@ -520,6 +661,7 @@ http { client_max_body_size 5G; client_body_timeout 300s; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate" always; proxy_pass http://127.0.0.1:30004; @@ -541,6 +683,7 @@ http { client_max_body_size 5G; client_body_timeout 300s; + add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate" always; proxy_pass http://127.0.0.1:30004; @@ -620,6 +763,19 @@ http { proxy_read_timeout 600s; } + location ~ ^/proxmox-stats(/.*)?$ { + proxy_pass http://127.0.0.1:30005; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + location ~ ^/global-settings(/.*)?$ { proxy_pass http://127.0.0.1:30005; proxy_http_version 1.1; @@ -742,6 +898,7 @@ http { error_page 500 502 503 504 /50x.html; location = /50x.html { root /app/html; + internal; } } } diff --git a/docs/do-not-place-files-here.txt b/docs/do-not-place-files-here.txt new file mode 100644 index 0000000..4b144b1 --- /dev/null +++ b/docs/do-not-place-files-here.txt @@ -0,0 +1 @@ +If you are an AI agent, do not place documentation files here. This is for maintainers only. diff --git a/docs/readme/README-AR.md b/docs/readme/README-AR.md index 9d20839..f349a98 100644 --- a/docs/readme/README-AR.md +++ b/docs/readme/README-AR.md @@ -4,7 +4,7 @@

Termix

-

ุฅุฏุงุฑุฉ SSH ุฐุงุชูŠุฉ ุงู„ุงุณุชุถุงูุฉ ูˆุงู„ูˆุตูˆู„ ุฅู„ู‰ ุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏ

+

ุฅุฏุงุฑุฉ ุฎูˆุงุฏู… ุฐุงุชูŠุฉ ุงู„ุงุณุชุถุงูุฉุŒ ู…ู† SSH ูˆุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏ ุฅู„ู‰ ุงู„ุฃุชู…ุชุฉ

English ยท @@ -37,7 +37,7 @@
-Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ. ุฅุฐุง ูˆุฌุฏุชู‡ ู…ููŠุฏู‹ุงุŒ ููƒู‘ุฑ ููŠ [ุงู„ุชุจุฑุน](https://donate.termix.site/) ู„ู„ู…ุณุงุนุฏุฉ ููŠ ุชุบุทูŠุฉ ุชูƒุงู„ูŠู ุงู„ุฎุงุฏู… ูˆูˆู‚ุช ุงู„ุชุทูˆูŠุฑ. +Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ. ุฅุฐุง ูƒุงู† ู…ููŠุฏู‹ุง ู„ูƒุŒ ููƒู‘ุฑ ููŠ [ุงู„ุชุจุฑุน](https://donate.termix.site/) ู„ู„ู…ุณุงุนุฏุฉ ููŠ ุชุบุทูŠุฉ ุชูƒุงู„ูŠู ุงู„ุฎูˆุงุฏู… ูˆูˆู‚ุช ุงู„ุชุทูˆูŠุฑ.
@@ -58,7 +58,7 @@ Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ. ุฅุฐุง ูˆุฌุฏุชู‡ ู…ููŠุฏู‹ุงุŒ ู ## ู†ุธุฑุฉ ุนุงู…ุฉ -Termix ู‡ูŠ ู…ู†ุตุฉ ู…ูุชูˆุญุฉ ุงู„ู…ุตุฏุฑ ูˆู…ุฌุงู†ูŠุฉ ู„ู„ุฃุจุฏ ูˆุฐุงุชูŠุฉ ุงู„ุงุณุชุถุงูุฉ ู„ุฅุฏุงุฑุฉ ุงู„ุฎูˆุงุฏู… ุจุดูƒู„ ุดุงู…ู„. ุชูˆูุฑ ุญู„ุงู‹ ู…ุชุนุฏุฏ ุงู„ู…ู†ุตุงุช ู„ุฅุฏุงุฑุฉ ุฎูˆุงุฏู…ูƒ ูˆุจู†ูŠุชูƒ ุงู„ุชุญุชูŠุฉ ู…ู† ุฎู„ุงู„ ูˆุงุฌู‡ุฉ ูˆุงุญุฏุฉ ูˆุณู‡ู„ุฉ ุงู„ุงุณุชุฎุฏุงู…. ูŠูˆูุฑ Termix ุงู„ูˆุตูˆู„ ุฅู„ู‰ ุทุฑููŠุฉ SSHุŒ ูˆุงู„ุชุญูƒู… ููŠ ุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏ (RDPุŒ VNCุŒ Telnet)ุŒ ูˆู‚ุฏุฑุงุช ุฅู†ุดุงุก ุฃู†ูุงู‚ SSHุŒ ูˆุฅุฏุงุฑุฉ ู…ู„ูุงุช SSH ุนู† ุจูุนุฏุŒ ูˆุงู„ุนุฏูŠุฏ ู…ู† ุงู„ุฃุฏูˆุงุช ุงู„ุฃุฎุฑู‰. ูŠูุนุฏ Termix ุงู„ุจุฏูŠู„ ุงู„ู…ุซุงู„ูŠ ุงู„ู…ุฌุงู†ูŠ ูˆุฐุงุชูŠ ุงู„ุงุณุชุถุงูุฉ ู„ู€ Termius ุงู„ู…ุชุงุญ ู„ุฌู…ูŠุน ุงู„ู…ู†ุตุงุช. +Termix ู…ู†ุตุฉ ู…ุฌุงู†ูŠุฉ ูˆู…ูุชูˆุญุฉ ุงู„ู…ุตุฏุฑ ูˆุฐุงุชูŠุฉ ุงู„ุงุณุชุถุงูุฉ ู„ุฅุฏุงุฑุฉ ุฎูˆุงุฏู…ูƒ. ุชุฌู…ุน ููŠ ู…ูƒุงู† ูˆุงุญุฏ ุทุฑููŠุงุช SSH ูˆุฃุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏุฉ (RDP ูˆVNC ูˆTelnet) ูˆู†ู‚ู„ ุงู„ู…ู„ูุงุช ูˆุงู„ุฃู†ูุงู‚ ูˆDocker ูˆุงู„ู…ู‚ุงูŠูŠุณ ูˆุงู„ุฃุชู…ุชุฉุŒ ุนู„ู‰ ุงู„ูˆูŠุจ ูˆุณุทุญ ุงู„ู…ูƒุชุจ ูˆุงู„ู‡ุงุชู. ุฅู†ู‡ ุจุฏูŠู„ ุฐุงุชูŠ ุงู„ุงุณุชุถุงูุฉ ู„ู€ Termius ูˆูŠุจู‚ู‰ ู…ุฌุงู†ูŠู‹ุง ุฅู„ู‰ ุงู„ุฃุจุฏ.
@@ -68,42 +68,42 @@ Termix ู‡ูŠ ู…ู†ุตุฉ ู…ูุชูˆุญุฉ ุงู„ู…ุตุฏุฑ ูˆู…ุฌุงู†ูŠุฉ ู„ู„ุฃุจุฏ ูˆุฐุง -**ุงู„ูˆุตูˆู„ ุฅู„ู‰ ุทุฑููŠุฉ SSH:** -ุทุฑููŠุฉ ูƒุงู…ู„ุฉ ุงู„ู…ูŠุฒุงุช ู…ุน ุฏุนู… ุชู‚ุณูŠู… ุงู„ุดุงุดุฉ (ุญุชู‰ 4 ู„ูˆุญุงุช) ู…ุน ู†ุธุงู… ุนู„ุงู…ุงุช ุชุจูˆูŠุจ ุดุจูŠู‡ ุจุงู„ู…ุชุตูุญ. ูŠุชุถู…ู† ุฏุนู… ุชุฎุตูŠุต ุงู„ุทุฑููŠุฉ ุจู…ุง ููŠ ุฐู„ูƒ ุณู…ุงุช ุงู„ุทุฑููŠุฉ ุงู„ุดุงุฆุนุฉ ูˆุงู„ุฎุทูˆุท ูˆุงู„ู…ูƒูˆู†ุงุช ุงู„ุฃุฎุฑู‰. +**ุทุฑููŠุฉ SSH:** +ุทุฑููŠุฉ ูƒุงู…ู„ุฉ ุจุนู„ุงู…ุงุช ุชุจูˆูŠุจ ู…ุซู„ ุงู„ู…ุชุตูุญ ูˆุชู‚ุณูŠู… ู„ู„ุดุงุดุฉุŒ ุญุชู‰ 6 ู„ูˆุญุงุช ููŠ ูˆู‚ุช ูˆุงุญุฏ. ุงุฎุชุฑ ุงู„ุณู…ุฉ ูˆุงู„ุฎุท ูˆุงู„ุฃู„ูˆุงู†. ูŠูˆุฌุฏ ููˆู‚ ูƒู„ ุฌู„ุณุฉ ุดุฑูŠุท ูŠุนุฑุถ ุงู„ู…ุนุงู„ุฌ ูˆุงู„ุฐุงูƒุฑุฉ ูˆุงู„ู‚ุฑุต ู„ุญุธูŠู‹ุงุŒ ู…ุน ุฑูˆุงุจุท ุณุฑูŠุนุฉ ุฅู„ู‰ ู…ู„ูุงุช ุฐู„ูƒ ุงู„ู…ุถูŠู ูˆDocker ูˆุงู„ุฃู†ูุงู‚ ูˆุงู„ู…ู‚ุงูŠูŠุณ. -**ุงู„ูˆุตูˆู„ ุฅู„ู‰ ุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏ:** -ุฏุนู… RDP ูˆ VNC ูˆ Telnet ุนุจุฑ ุงู„ู…ุชุตูุญ ู…ุน ุชุฎุตูŠุต ูƒุงู…ู„ ูˆุชู‚ุณูŠู… ุงู„ุดุงุดุฉ. +**ุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏ:** +RDP ูˆVNC ูˆTelnet ุฏุงุฎู„ ุงู„ู…ุชุตูุญุŒ ููŠ ุนู„ุงู…ุงุช ุชุจูˆูŠุจ ูˆุดุงุดุฉ ู…ู‚ุณู‘ู…ุฉ ู…ุซู„ ุฃูŠ ุฌู„ุณุฉ ุฃุฎุฑู‰. ูŠุชุถู…ู† ู…ุชุตูุญ ู…ู„ูุงุช ู„ุฃู‚ุฑุงุต RDP ูˆุฑูุนู‹ุง ุจุงู„ุณุญุจ ูˆุงู„ุฅูู„ุงุช. ุนู„ู‰ ุณุทุญ ู…ูƒุชุจ Windows ูŠู…ูƒู†ูƒ ุฃูŠุถู‹ุง ูุชุญ ุงู„ู…ุถูŠู ููŠ ุนู…ูŠู„ RDP ุงู„ุฃุตู„ูŠ. -**ุฅุฏุงุฑุฉ ุฃู†ูุงู‚ SSH:** -ุฅู†ุดุงุก ูˆุฅุฏุงุฑุฉ ุฃู†ูุงู‚ SSH ุจูŠู† ุงู„ุฎูˆุงุฏู… ู…ุน ุฅุนุงุฏุฉ ุงู„ุงุชุตุงู„ ุงู„ุชู„ู‚ุงุฆูŠ ูˆู…ุฑุงู‚ุจุฉ ุงู„ุญุงู„ุฉ ูˆุฅุนุงุฏุฉ ุงู„ุชูˆุฌูŠู‡ ุงู„ู…ุญู„ูŠ ุฃูˆ ุงู„ุจุนูŠุฏ ุฃูˆ SOCKS ุงู„ุฏูŠู†ุงู…ูŠูƒูŠ. ูŠุชู… ุชุฎุฒูŠู† ุฅุนุฏุงุฏุงุช ู†ูู‚ ุงู„ุนู…ูŠู„-ุงู„ู…ูƒุชุจูŠ ุฅู„ู‰ ุงู„ุณูŠุฑูุฑ ู…ุญู„ูŠุงู‹ ู„ูƒู„ ุชุซุจูŠุช ู…ูƒุชุจูŠุŒ ูˆูŠู…ูƒู† ุญูุธ ู„ู‚ุทุงุช C2S ุงู„ุงุฎุชูŠุงุฑูŠุฉ ุนู„ู‰ ุงู„ุฎุงุฏู… ูˆุฅุนุงุฏุฉ ุชุณู…ูŠุชู‡ุง ูˆุชุญู…ูŠู„ู‡ุง ุฃูˆ ุญุฐูู‡ุง ุนู†ุฏู…ุง ุชุฑูŠุฏ ู†ู‚ู„ ุชูƒูˆูŠู† ุงู„ู†ูู‚ ุงู„ู…ุญู„ูŠ ุจูŠู† ุงู„ุนู…ู„ุงุก. +**ุฃู†ูุงู‚ SSH:** +ุฅุนุงุฏุฉ ุชูˆุฌูŠู‡ ู…ุญู„ูŠุฉ ูˆุจุนูŠุฏุฉ ูˆSOCKS ุฏูŠู†ุงู…ูŠูƒูŠุฉุŒ ู…ุน ุฅุนุงุฏุฉ ุงุชุตุงู„ ุชู„ู‚ุงุฆูŠุฉ ูˆูุญูˆุต ู„ู„ุญุงู„ุฉ. ุชูุญูุธ ุฃู†ูุงู‚ ุงู„ุนู…ูŠู„ ุฅู„ู‰ ุงู„ุฎุงุฏู… ููŠ ุชุทุจูŠู‚ ุณุทุญ ุงู„ู…ูƒุชุจ ุนู„ู‰ ุฐู„ูƒ ุงู„ุฌู‡ุงุฒุŒ ูˆูŠู…ูƒู†ูƒ ุญูุธ ุฅุนุฏุงุฏุงุช ุฌุงู‡ุฒุฉ ุนู„ู‰ ุงู„ุฎุงุฏู… ู„ู†ู‚ู„ ุงู„ุชู‡ูŠุฆุฉ ุฅู„ู‰ ุฌู‡ุงุฒ ุขุฎุฑ. -**ู…ุฏูŠุฑ ุงู„ู…ู„ูุงุช ุนู† ุจูุนุฏ:** -ุฅุฏุงุฑุฉ ุงู„ู…ู„ูุงุช ู…ุจุงุดุฑุฉ ุนู„ู‰ ุงู„ุฎูˆุงุฏู… ุงู„ุจุนูŠุฏุฉ ู…ุน ุฏุนู… ุนุฑุถ ูˆุชุญุฑูŠุฑ ุงู„ูƒูˆุฏ ูˆุงู„ุตูˆุฑ ูˆุงู„ุตูˆุช ูˆุงู„ููŠุฏูŠูˆ. ุฑูุน ูˆุชู†ุฒูŠู„ ูˆุฅุนุงุฏุฉ ุชุณู…ูŠุฉ ูˆุญุฐู ูˆู†ู‚ู„ ุงู„ู…ู„ูุงุช ุจุณู„ุงุณุฉ ู…ุน ุฏุนู… sudo. ูŠุชุถู…ู† ุฏุนู… ู†ู‚ู„ ุงู„ู…ู„ูุงุช ู…ู† ุฎุงุฏู… ุฅู„ู‰ ุขุฎุฑ. +**ู…ุฏูŠุฑ ุงู„ู…ู„ูุงุช:** +ุชุตูุญ ุงู„ู…ู„ูุงุช ูˆุญุฑู‘ุฑู‡ุง ูˆุงุฑูุนู‡ุง ูˆู†ุฒู‘ู„ู‡ุง ูˆุฃุนุฏ ุชุณู…ูŠุชู‡ุง ูˆุงู†ู‚ู„ู‡ุง ูˆุงุญุฐูู‡ุง ุนุจุฑ SFTPุŒ ู…ุน ุฏุนู… sudo. ุงุนุฑุถ ูˆุญุฑู‘ุฑ ุงู„ุดูŠูุฑุฉ ูˆุงู„ุตูˆุฑ ูˆุงู„ุตูˆุช ูˆุงู„ููŠุฏูŠูˆ. ุงู†ุณุฎ ุงู„ู…ู„ูุงุช ู…ุจุงุดุฑุฉ ู…ู† ุฎุงุฏู… ุฅู„ู‰ ุขุฎุฑุŒ ู…ุน ุงุฎุชูŠุงุฑ ุฃุณุฑุน ู…ุณุงุฑ ุชู„ู‚ุงุฆูŠู‹ุง ูˆุงู„ุชุญู‚ู‚ ู…ู† ุณู„ุงู…ุฉ ุงู„ู†ู‚ู„. -**ุฅุฏุงุฑุฉ Docker ูˆ Podman:** -ุชุดุบูŠู„ ูˆุฅูŠู‚ุงู ูˆุชุนู„ูŠู‚ ูˆุญุฐู ุงู„ุญุงูˆูŠุงุช. ุนุฑุถ ุฅุญุตุงุฆูŠุงุช ุงู„ุญุงูˆูŠุงุช. ุงู„ุชุญูƒู… ููŠ ุงู„ุญุงูˆูŠุฉ ุจุงุณุชุฎุฏุงู… ุทุฑููŠุฉ docker exec. ูŠุฏุนู… ูƒู„ุงู‹ ู…ู† Docker ูˆ Podman ูƒุจูŠุฆุฉ ุชุดุบูŠู„ ู„ู„ุญุงูˆูŠุงุช. ู„ู… ูŠูุตู…ู… ู„ูŠุญู„ ู…ุญู„ Portainer ุฃูˆ Dockge ุจู„ ู„ุฅุฏุงุฑุฉ ุญุงูˆูŠุงุชูƒ ุจุจุณุงุทุฉ ู…ู‚ุงุฑู†ุฉ ุจุฅู†ุดุงุฆู‡ุง. +**Docker ูˆPodman:** +ุดุบู‘ู„ ุงู„ุญุงูˆูŠุงุช ูˆุฃูˆู‚ูู‡ุง ูˆุนู„ู‘ู‚ู‡ุง ูˆุงุญุฐูู‡ุงุŒ ูˆุชุงุจุน ุฅุญุตุงุกุงุชู‡ุงุŒ ูˆุงูุชุญ ุทุฑููŠุฉ ุฏุงุฎู„ ุฅุญุฏุงู‡ุง. ูŠุนู…ู„ ู…ุน Docker ูˆPodman ู…ุนู‹ุง. ู„ูŠุณ ุจุฏูŠู„ุงู‹ ุนู† Portainer ุฃูˆ DockgeุŒ ุจู„ ูˆุณูŠู„ุฉ ู„ุฅุฏุงุฑุฉ ุงู„ุญุงูˆูŠุงุช ุงู„ู…ูˆุฌูˆุฏุฉ ู„ุฏูŠูƒ. -**ู…ุฏูŠุฑ ู…ุถูŠูุงุช SSH:** -ุญูุธ ูˆุชู†ุธูŠู… ูˆุฅุฏุงุฑุฉ ุงุชุตุงู„ุงุช SSH ุงู„ุฎุงุตุฉ ุจูƒ ุจุงุณุชุฎุฏุงู… ุงู„ุนู„ุงู…ุงุช ูˆุงู„ู…ุฌู„ุฏุงุช (ู…ุน ุฏุนู… ุชุฎุตูŠุต ุงู„ู…ุฌู„ุฏุงุช ูˆุงู„ู…ุฌู„ุฏุงุช ุงู„ู…ุชุฏุงุฎู„ุฉ)ุŒ ูˆุญูุธ ุจูŠุงู†ุงุช ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ุงู„ู‚ุงุจู„ุฉ ู„ุฅุนุงุฏุฉ ุงู„ุงุณุชุฎุฏุงู… ุจุณู‡ูˆู„ุฉ ู…ุน ุฅู…ูƒุงู†ูŠุฉ ุฃุชู…ุชุฉ ู†ุดุฑ ู…ูุงุชูŠุญ SSH. +**ู…ุฏูŠุฑ ุงู„ู…ุถูŠูุงุช:** +ุงุญูุธ ู…ุถูŠูุงุชูƒ ูˆู†ุธู‘ู…ู‡ุง ุจุงู„ูˆุณูˆู… ูˆู…ุฌู„ุฏุงุช ู…ุชุฏุงุฎู„ุฉ ูŠู…ูƒู†ูƒ ุชุณู…ูŠุชู‡ุง ูˆุชู„ูˆูŠู†ู‡ุง. ุฃุนุฏ ุงุณุชุฎุฏุงู… ุจูŠุงู†ุงุช ุงู„ุฏุฎูˆู„ ุงู„ู…ุญููˆุธุฉ ุนุจุฑ ุนุฏุฉ ู…ุถูŠูุงุชุŒ ูˆุงู†ุดุฑ ู…ูุงุชูŠุญ SSH ุชู„ู‚ุงุฆูŠู‹ุงุŒ ูˆุงุฌู…ุน ุงู„ู…ุถูŠูุงุช ุชุญุช ู…ุถูŠู ุฑุฆูŠุณูŠุŒ ูˆุญุฑู‘ุฑ ูˆุตุฏู‘ุฑ ุฏูุนุฉ ูˆุงุญุฏุฉุŒ ูˆุงุณุชุฎุฏู… ุงู„ุงุชุตุงู„ ุงู„ุณุฑูŠุน ู„ู„ุงุชุตุงู„ุงุช ุงู„ุนุงุจุฑุฉ ุงู„ุชูŠ ู„ุง ุชุฑูŠุฏ ุญูุธู‡ุง. @@ -111,83 +111,139 @@ Termix ู‡ูŠ ู…ู†ุตุฉ ู…ูุชูˆุญุฉ ุงู„ู…ุตุฏุฑ ูˆู…ุฌุงู†ูŠุฉ ู„ู„ุฃุจุฏ ูˆุฐุง **ู…ู‚ุงูŠูŠุณ ุงู„ู…ุถูŠู:** -ุนุฑุถ ุงุณุชุฎุฏุงู… ุงู„ู…ุนุงู„ุฌ ูˆุงู„ุฐุงูƒุฑุฉ ูˆุงู„ู‚ุฑุต ูˆุงู„ุดุจูƒุฉ ูˆูˆู‚ุช ุงู„ุชุดุบูŠู„ ูˆู…ุนู„ูˆู…ุงุช ุงู„ู†ุธุงู… ูˆุฌุฏุงุฑ ุงู„ุญู…ุงูŠุฉ ูˆู…ุฑุงู‚ุจ ุงู„ู…ู†ุงูุฐ ูˆุนุงุฑุถ ุงู„ุณุฌู„ุงุช ูˆุงู„ู…ุณุชุฎุฏู…ูŠู†/ุงู„ุตู„ุงุญูŠุงุช ูˆุงู„ุดู‡ุงุฏุงุช ูˆุบูŠุฑู‡ุง ุงู„ูƒุซูŠุฑุŒ ุชุนู…ู„ ุนู„ู‰ ู…ุนุธู… ุงู„ุฎูˆุงุฏู… ุงู„ู…ุจู†ูŠุฉ ุนู„ู‰ Linux. ูŠุชุถู…ู† ุฑุณูˆู… ุจูŠุงู†ูŠุฉ ุชุงุฑูŠุฎูŠุฉ ุฒู…ู†ูŠุฉ ุงู„ุณู„ุณู„ุฉ ูˆุชู†ุจูŠู‡ุงุช ู‚ุงุฆู…ุฉ ุนู„ู‰ ุงู„ุญุฏูˆุฏ ู…ุน ุฏุนู… ntfy ูˆุงู„ู€ webhook. +ุงู„ู…ุนุงู„ุฌ ูˆุงู„ุฐุงูƒุฑุฉ ูˆุงู„ู‚ุฑุต ูˆุงู„ุดุจูƒุฉ ูˆุงู„ุญุฑุงุฑุฉ ูˆู…ุฏุฉ ุงู„ุชุดุบูŠู„ ูˆุงู„ุนู…ู„ูŠุงุช ูˆุงู„ู…ู†ุงูุฐ ูˆุชุณุฌูŠู„ุงุช ุงู„ุฏุฎูˆู„ ูˆู…ุนู„ูˆู…ุงุช ุงู„ู†ุธุงู… ุนู„ู‰ ู…ุนุธู… ุฎูˆุงุฏู… LinuxุŒ ู…ุน ุฑุณูˆู… ุจูŠุงู†ูŠุฉ ู„ู„ุณุฌู„. ุชุชูŠุญ ู„ูƒ ุจุทุงู‚ุงุช ุงู„ุฅุฏุงุฑุฉ ุงู„ุชุนุงู…ู„ ู…ุน ุงู„ุฎุฏู…ุงุช ูˆู…ู‡ุงู… cron ูˆุงู„ุญุฒู… ูˆุงู„ู…ุณุชุฎุฏู…ูŠู† ูˆู‚ูˆุงุนุฏ ุงู„ุฌุฏุงุฑ ุงู„ู†ุงุฑูŠ ูˆWireGuard ูˆTailscale ูˆุดู‡ุงุฏุงุช SSL ูˆุงู„ุณุฌู„ุงุช ูˆูุญูˆุต ุงู„ุณู„ุงู…ุฉ ุฏูˆู† ู…ุบุงุฏุฑุฉ Termix. -**ู…ุตุงุฏู‚ุฉ ุงู„ู…ุณุชุฎุฏู…ูŠู†:** -ุฅุฏุงุฑุฉ ุขู…ู†ุฉ ู„ู„ู…ุณุชุฎุฏู…ูŠู† ู…ุน ุถูˆุงุจุท ุฅุฏุงุฑูŠุฉ (ูŠู…ูƒู† ุชุนุฏูŠู„ ู…ุนู„ูˆู…ุงุช ุงู„ู…ุณุชุฎุฏู…ูŠู† ุงู„ุขุฎุฑูŠู†) ูˆุฏุนู… OIDC/LDAP/SSO (ู…ุน ุงู„ุชุญูƒู… ููŠ ุงู„ูˆุตูˆู„) ูˆ 2FA (TOTP) ูˆุฏุนู… ู…ูุงุชูŠุญ ุงู„ู…ุฑูˆุฑ (WebAuthn). ุนุฑุถ ุฌู„ุณุงุช ุงู„ู…ุณุชุฎุฏู…ูŠู† ุงู„ู†ุดุทุฉ ุนุจุฑ ุฌู…ูŠุน ุงู„ู…ู†ุตุงุช ูˆุฅู„ุบุงุก ุงู„ุตู„ุงุญูŠุงุช. ุฑุจุท ุญุณุงุจุงุช OIDC/ุงู„ู…ุญู„ูŠุฉ ู…ุนุงู‹. ุนุฑุถ ุณุฌู„ ุชุฏู‚ูŠู‚ ู„ุฌู…ูŠุน ุฅุฌุฑุงุกุงุช ุงู„ู…ุณุชุฎุฏู…ูŠู†. +**ุงู„ุฃุชู…ุชุฉ:** +ุงุฎุชุฑ ู…ูุดุบูู‘ู„ู‹ุง ุซู… ุญุฏู‘ุฏ ู…ุง ูŠู†ุจุบูŠ ุฃู† ูŠุญุฏุซ. ุชุดู…ู„ ุงู„ู…ุดุบู‘ู„ุงุช ุชุฌุงูˆุฒ ู…ู‚ูŠุงุณ ู„ุญุฏ ู…ุนูŠู†ุŒ ุฃูˆ ู…ุถูŠูู‹ุง ูŠุณู‚ุท ุฃูˆ ูŠุนูˆุฏุŒ ุฃูˆ ุชุบูŠู‘ุฑ ูุญุต ุงู„ุณู„ุงู…ุฉุŒ ุฃูˆ ุฌุฏูˆู„ู‹ุง ุฒู…ู†ูŠู‹ุงุŒ ุฃูˆ ุญุฏุซ ุญุงูˆูŠุฉุŒ ุฃูˆ webhook ูˆุงุฑุฏู‹ุง. ูŠู…ูƒู† ู„ู„ุฎุทูˆุงุช ุชุดุบูŠู„ ุฃูˆุงู…ุฑ ูˆู…ู‚ุชุทูุงุชุŒ ูˆุงู„ุชุญูƒู… ููŠ ุงู„ุญุงูˆูŠุงุช ูˆุงู„ุฃู†ูุงู‚ุŒ ูˆุฅูŠู‚ุงุธ ู…ุถูŠูุŒ ูˆุงุณุชุฏุนุงุก ุฑุงุจุทุŒ ูˆุงู„ุงู†ุชุธุงุฑุŒ ูˆุงู„ุชูุฑุน ุญุณุจ ุดุฑุทุŒ ูˆุชุดุบูŠู„ ุฃุชู…ุชุฉ ุฃุฎุฑู‰ุŒ ูˆุฅุดุนุงุฑูƒ ุนุจุฑ ntfy ุฃูˆ Discord ุฃูˆ webhook. ุชุชูŠุญ ู„ูƒ ุนู…ู„ูŠุงุช ุงู„ุชุดุบูŠู„ ุงู„ุชุฌุฑูŠุจูŠ ุงู„ุชุฌุฑุจุฉ ุจุฃู…ุงู† ุฃูˆู„ู‹ุง. -**ุชูƒุงู…ู„ Tailscale:** -ุนุฑุถ ุฃุฌู‡ุฒุฉ ุดุจูƒุชูƒ ู…ู† Tailscale ู„ุฅุถุงูุชู‡ุง ุจุณุฑุนุฉ ูƒู…ุถูŠูุงุชุŒ ูˆุงู„ุงุชุตุงู„ ุนุจุฑ Tailscale SSH ูƒุทุฑูŠู‚ุฉ ู…ุตุงุฏู‚ุฉุŒ ู…ู…ุง ูŠุชูŠุญ ู„ู‚ูˆุงุฆู… ุชุญูƒู… ุงู„ูˆุตูˆู„ ููŠ Tailscale ุงู„ุชุนุงู…ู„ ู…ุน ุงู„ุชููˆูŠุถ ุฏูˆู† ุงู„ุญุงุฌุฉ ู„ุชุฎุฒูŠู† ุจูŠุงู†ุงุช ุงุนุชู…ุงุฏ. +**ุงู„ุฃุณุงุทูŠู„:** +ุงุฌู…ุน ุงู„ู…ุถูŠูุงุช ููŠ ุฃุณุทูˆู„ ุจุงุฎุชูŠุงุฑู‡ุง ูŠุฏูˆูŠู‹ุง ุฃูˆ ุจู‚ูˆุงุนุฏ ุงู„ูˆุณูˆู…ุŒ ู„ุชู†ุถู… ุงู„ู…ุถูŠูุงุช ุงู„ุฌุฏูŠุฏุฉ ุชู„ู‚ุงุฆูŠู‹ุง. ุดุบู‘ู„ ุฃู…ุฑู‹ุง ูˆุงุญุฏู‹ุง ุนู„ู‰ ูƒู„ ุงู„ู…ุถูŠูุงุช ุฏูุนุฉ ูˆุงุญุฏุฉุŒ ูˆุงุฏูุน ุงู„ู…ู„ูุงุช ูˆุงุณุญุจู‡ุง ู…ู† ุฌู…ูŠุนู‡ุงุŒ ูˆุซุจู‘ุช ุงู„ุญุฒู…ุŒ ูˆุงุฌู…ุน ุฌุฑุฏู‹ุง ุจู†ุธุงู… ุงู„ุชุดุบูŠู„ ูˆุงู„ู†ูˆุงุฉ ูˆุงู„ู…ุนู…ุงุฑูŠุฉ ูˆู…ุฏุฉ ุงู„ุชุดุบูŠู„. -**RBAC/ุงู„ู…ุดุงุฑูƒุฉ:** -ุฅู†ุดุงุก ุงู„ุฃุฏูˆุงุฑ ูˆู…ุดุงุฑูƒุฉ ุงู„ู…ุถูŠูุงุช ุนุจุฑ ุงู„ู…ุณุชุฎุฏู…ูŠู†/ุงู„ุฃุฏูˆุงุฑ. ูŠุฏุนู… ุฌู…ูŠุน ุฃู†ูˆุงุน ุงู„ู…ุตุงุฏู‚ุฉ ูˆุฌู…ูŠุน ุจุฑูˆุชูˆูƒูˆู„ุงุช ุงู„ู…ุถูŠู. +**ู…ุณุงุนุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ:** +ู…ูŠุฒุฉ ุงุฎุชูŠุงุฑูŠุฉ ูˆู…ุนุทู„ุฉ ุญุชู‰ ุชูุนู‘ู„ู‡ุง ุจู†ูุณูƒ. ุงุฑุจุท OpenAI ุฃูˆ Anthropic ุฃูˆ Gemini ุฃูˆ Ollama ุฃูˆ ุฃูŠ ู†ู‚ุทุฉ ูˆุตูˆู„ ู…ุชูˆุงูู‚ุฉ ู…ุน OpenAI ูˆุงุณุฃู„ ุนู† ุฅุนุฏุงุฏุงุชูƒ. ูŠู…ูƒู†ู‡ ู‚ุฑุงุกุฉ ุงู„ู…ุถูŠูุงุช ูˆุงู„ุฃุณุงุทูŠู„ ูˆุงู„ู…ู‚ุชุทูุงุช ูˆุงู„ุชู†ุจูŠู‡ุงุชุŒ ูˆูŠู‚ุชุฑุญ ุงู„ุชุบูŠูŠุฑุงุช ู„ุชูˆุงูู‚ ุนู„ูŠู‡ุง ุจุฏู„ู‹ุง ู…ู† ุชู†ููŠุฐู‡ุง ุจู†ูุณู‡. ู„ุง ูŠู…ูƒู†ู‡ ุฃุจุฏู‹ุง ุงู„ูˆุตูˆู„ ุฅู„ู‰ ุจูŠุงู†ุงุช ุงู„ุฏุฎูˆู„ ุฃูˆ ุงู„ู…ุณุชุฎุฏู…ูŠู† ุฃูˆ ุงู„ุฅุนุฏุงุฏุงุช. ูŠุณุชุทูŠุน ุงู„ู…ุณุคูˆู„ูˆู† ุชุนุทูŠู„ู‡ ู„ู„ู†ุธุงู… ุจุงู„ูƒุงู…ู„ุŒ ูˆูŠู…ูƒู†ูƒ ุฅุฎูุงุคู‡ ุฃุซู†ุงุก ุงู„ุฅุนุฏุงุฏ. -**ุงู„ุงุชุตุงู„ุงุช ุงู„ุชุณู„ุณู„ูŠุฉ:** -ุงู„ุงุชุตุงู„ ุจุงู„ุฃุฌู‡ุฒุฉ ุงู„ุชุณู„ุณู„ูŠุฉ (ุฃุฌู‡ุฒุฉ ุงู„ุชูˆุฌูŠู‡ ูˆุงู„ู…ูุงุชูŠุญ ูˆุงู„ู…ุชุญูƒู…ุงุช ุงู„ุฏู‚ูŠู‚ุฉ ูˆุบูŠุฑู‡ุง) ู…ุจุงุดุฑุฉ ู…ู† ุงู„ู…ุชุตูุญ ุฃูˆ ุชุทุจูŠู‚ ุณุทุญ ุงู„ู…ูƒุชุจ. ุถุจุท ู…ุนุฏู„ ู†ู‚ู„ ุงู„ุจูŠุงู†ุงุช ูˆุจุชุงุช ุงู„ุจูŠุงู†ุงุช ูˆุจุชุงุช ุงู„ุชูˆู‚ู ูˆุงู„ุชูƒุงูุค. ูŠุณุชุฎุฏู… Web Serial API ููŠ ุงู„ู…ุชุตูุญุงุช ุงู„ู…ุฏุนูˆู…ุฉ ุฃูˆ ุฎู„ููŠุฉ ุฃุตู„ูŠุฉ ููŠ ุชุทุจูŠู‚ Electron. +**ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ูˆุงู„ู…ุณุชุฎุฏู…ูˆู†:** +ุญุณุงุจุงุช ู…ุญู„ูŠุฉ ุฅุถุงูุฉ ุฅู„ู‰ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ุนุจุฑ OIDC ูˆLDAP ูˆGitHub ูˆGoogleุŒ ู…ุน ุงู„ุชุญู‚ู‚ ุจุฎุทูˆุชูŠู† (TOTP) ูˆู…ูุงุชูŠุญ ุงู„ู…ุฑูˆุฑ (WebAuthn) ูˆุงู„ุฃุฌู‡ุฒุฉ ุงู„ู…ูˆุซูˆู‚ุฉ. ูŠุณุชุทูŠุน ุงู„ู…ุณุคูˆู„ูˆู† ุฅุฏุงุฑุฉ ุงู„ู…ุณุชุฎุฏู…ูŠู† ูˆุฑุจุท ู…ุฌู…ูˆุนุงุช OIDC ุจุงู„ุฃุฏูˆุงุฑ ูˆุฑุคูŠุฉ ูƒู„ ุงู„ุฌู„ุณุงุช ุงู„ู†ุดุทุฉ ุนู„ู‰ ุฌู…ูŠุน ุงู„ู…ู†ุตุงุช ูˆุฅู„ุบุงุคู‡ุง. ุงุฑุจุท ุญุณุงุจูƒ ุงู„ู…ุญู„ูŠ ุจุญุณุงุจ OIDCุŒ ูˆุงุทู‘ู„ุน ุนู„ู‰ ุณุฌู„ ุงู„ุชุฏู‚ูŠู‚ ู„ู…ุง ูุนู„ู‡ ุงู„ุฌู…ูŠุน. +**ุงู„ุฃุฏูˆุงุฑ ูˆุงู„ู…ุดุงุฑูƒุฉ:** +ุฃู†ุดุฆ ุฃุฏูˆุงุฑู‹ุง ูˆุดุงุฑูƒ ุงู„ู…ุถูŠูุงุช ู…ุน ุงู„ู…ุณุชุฎุฏู…ูŠู† ุฃูˆ ุงู„ุฃุฏูˆุงุฑ ุนู„ู‰ ุฃุฑุจุนุฉ ู…ุณุชูˆูŠุงุช: ุงู„ุงุชุตุงู„ ูˆุงู„ุนุฑุถ ูˆุงู„ุชุญุฑูŠุฑ ูˆุงู„ุฅุฏุงุฑุฉ. ูŠุนู…ู„ ู…ุน ุฌู…ูŠุน ุฃู†ูˆุงุน ุงู„ู…ุตุงุฏู‚ุฉ ูˆุฌู…ูŠุน ุงู„ุจุฑูˆุชูˆูƒูˆู„ุงุชุŒ ูˆูŠู…ูƒู†ูƒ ุชุฌุงูˆุฒ ุจูŠุงู†ุงุช ุงู„ุฏุฎูˆู„ ุงู„ู…ุณุชุฎุฏู…ุฉ ู„ู…ุถูŠู ู…ุดุชุฑูƒ. + + + + + + **ุงู„ุชู†ุจูŠู‡ุงุช:** -ุถุจุท ู‚ูˆุงุนุฏ ุชู†ุจูŠู‡ ู‚ุงุฆู…ุฉ ุนู„ู‰ ุงู„ุญุฏูˆุฏ ู„ู…ู‚ุงูŠูŠุณ ุงู„ู…ุถูŠู (ุงู„ู…ุนุงู„ุฌ ูˆุงู„ุฐุงูƒุฑุฉ ูˆุงู„ู‚ุฑุต ูˆุบูŠุฑู‡ุง) ูˆุงู„ุญุตูˆู„ ุนู„ู‰ ุฅุดุนุงุฑุงุช ุนุจุฑ ntfy ุฃูˆ webhooks ุนู†ุฏ ุฅุทู„ุงู‚ู‡ุง. ุนุฑุถ ุงู„ุชู†ุจูŠู‡ุงุช ุงู„ู†ุดุทุฉ ูˆุงู„ู…ุญู„ูˆู„ุฉ ููŠ ุณุฌู„ ุงู„ุชุงุฑูŠุฎ. +ุถุน ู‚ูˆุงุนุฏ ุนู„ู‰ ู…ู‚ุงูŠูŠุณ ุงู„ู…ุถูŠู ู…ุซู„ ุงู„ู…ุนุงู„ุฌ ูˆุงู„ุฐุงูƒุฑุฉ ูˆุงู„ู‚ุฑุตุŒ ูˆุชู„ู‚ูŽู‘ ุฅุดุนุงุฑู‹ุง ุนุจุฑ ntfy ุฃูˆ Discord ุฃูˆ webhook ุนู†ุฏ ุชูุนูŠู„ู‡ุง. ุงุทู‘ู„ุน ุนู„ู‰ ุงู„ุชู†ุจูŠู‡ุงุช ุงู„ู†ุดุทุฉ ูˆุงู„ู…ู†ุชู‡ูŠุฉ ููŠ ุณุฌู„ุŒ ูˆุชุฌุงู‡ู„ ู…ุง ู„ุง ูŠู‡ู…ูƒ ู…ู†ู‡ุง. - - **ุงู„ุตูุญุฉ ุงู„ุฑุฆูŠุณูŠุฉ:** -ุตูุญุฉ ุฑุฆูŠุณูŠุฉ ู‚ุงุจู„ุฉ ู„ู„ุชุฎุตูŠุต ุจุงู„ูƒุงู…ู„ ู…ุน ุดุจูƒุฉ ุฃุฏูˆุงุช ู‚ุงุจู„ุฉ ู„ู„ุณุญุจ ูˆุงู„ุฅูู„ุงุช. ุฃุถู ุฃุฏูˆุงุช ู„ุญุงู„ุฉ ุงู„ู…ุถูŠู ูˆุฑูˆุงุจุท ุงู„ุฎุฏู…ุงุช ูˆุงู„ุณุงุนุงุช ูˆุงู„ู…ู„ุงุญุธุงุช ูˆุฎู„ุงุตุงุช RSS ูˆุงู„ุทู‚ุณ ูˆุญุงูˆูŠุงุช Docker ูˆู…ุฎุทุทุงุช ู…ู‚ุงูŠูŠุณ ุงู„ู…ุถูŠู ูˆุงู„ุทุฑููŠุงุช ุงู„ู…ุถู…ู†ุฉ ูˆุงู„ุฅุทุงุฑุงุช ุงู„ู…ุถู…ู†ุฉ ูˆุฃูƒุซุฑ. - - - - -**ุชุดููŠุฑ ู‚ุงุนุฏุฉ ุงู„ุจูŠุงู†ุงุช:** -ูŠูุฎุฒูŽู‘ู† ุงู„ุฎุงุฏู… ุงู„ุฎู„ููŠ ูƒู…ู„ูุงุช ู‚ุงุนุฏุฉ ุจูŠุงู†ุงุช SQLite ู…ุดูุฑุฉ. ุงุทู„ุน ุนู„ู‰ [ุงู„ูˆุซุงุฆู‚](https://docs.termix.site/security) ู„ู…ุฒูŠุฏ ู…ู† ุงู„ู…ุนู„ูˆู…ุงุช. +ุดุจูƒุฉ ุนู†ุงุตุฑ ุชุจู†ูŠู‡ุง ุจู†ูุณูƒ ุจุงู„ุณุญุจ ูˆุงู„ุฅูู„ุงุช. ู‡ู†ุงูƒ ุนู†ุงุตุฑ ู„ุญุงู„ุฉ ุงู„ู…ุถูŠูุงุช ูˆnping ูˆุฑูˆุงุจุท ุงู„ุฎุฏู…ุงุช ูˆุงู„ุฅุดุงุฑุงุช ุงู„ู…ุฑุฌุนูŠุฉ ูˆุงู„ุจุญุซ ูˆุงู„ุณุงุนุงุช ูˆุงู„ุชู‚ูˆูŠู…ุงุช ูˆุงู„ุนุฏ ุงู„ุชู†ุงุฒู„ูŠ ูˆุงู„ู…ู„ุงุญุธุงุช ูˆRSS ูˆุงู„ุทู‚ุณ ูˆุงู„ุตูˆุฑ ูˆุงู„ุฅุทุงุฑุงุช ุงู„ู…ุถู…ู‘ู†ุฉ ูˆDocker ูˆุงู„ุฃู†ูุงู‚ ูˆุฑุณูˆู… ุงู„ู…ู‚ุงูŠูŠุณ ูˆูˆุงุฌู‡ุงุช API ุงู„ุฎุงุตุฉ ุจูƒุŒ ูˆุญุชู‰ ุทุฑููŠุฉ ุญูŠุฉ. -**ุงู„ุฑุณู… ุงู„ุจูŠุงู†ูŠ ู„ู„ุดุจูƒุฉ:** -ุชุฎุตูŠุต ู„ูˆุญุฉ ุงู„ุชุญูƒู… ู„ุชุตูˆุฑ ู…ุฎุชุจุฑูƒ ุงู„ู…ู†ุฒู„ูŠ ุจู†ุงุกู‹ ุนู„ู‰ ุงุชุตุงู„ุงุช SSH ู…ุน ุฏุนู… ุงู„ุญุงู„ุฉ. +**ุงู„ู…ู‚ุชุทูุงุช ูˆุงู„ุฃุฏูˆุงุช:** +ุงุญูุธ ุงู„ุฃูˆุงู…ุฑ ุงู„ุชูŠ ุชุณุชุฎุฏู…ู‡ุง ูƒุซูŠุฑู‹ุง ูˆุดุบู‘ู„ู‡ุง ุจู†ู‚ุฑุฉ ูˆุงุญุฏุฉุŒ ู…ุน ู…ุชุบูŠุฑุงุช ู„ู„ู…ุถูŠู ูˆู„ู…ุฏุฎู„ุงุชูƒ ุงู„ุฎุงุตุฉ. ุดุบู‘ู„ ุฃู…ุฑู‹ุง ูˆุงุญุฏู‹ุง ุนู„ู‰ ูƒู„ ุงู„ุทุฑููŠุงุช ุงู„ู…ูุชูˆุญุฉุŒ ูˆุงุจุญุซ ููŠ ุณุฌู„ ุฃูˆุงู…ุฑูƒ ู…ุน ุงู„ุฅูƒู…ุงู„ ุงู„ุชู„ู‚ุงุฆูŠ. -**ุฃุฏูˆุงุช SSH:** -ุฅู†ุดุงุก ู…ู‚ุชุทูุงุช ุฃูˆุงู…ุฑ ู‚ุงุจู„ุฉ ู„ุฅุนุงุฏุฉ ุงู„ุงุณุชุฎุฏุงู… ุชูู†ููŽู‘ุฐ ุจู†ู‚ุฑุฉ ูˆุงุญุฏุฉ. ุชุดุบูŠู„ ุฃู…ุฑ ูˆุงุญุฏ ููŠ ูˆู‚ุช ูˆุงุญุฏ ุนุจุฑ ุนุฏุฉ ุทุฑููŠุงุช ู…ูุชูˆุญุฉ. +**ู…ุดุงุฑูƒุฉ ุงู„ุฌู„ุณุฉ:** +ุดุงุฑูƒ ุฌู„ุณุฉ ุทุฑููŠุฉ ุฃูˆ RDP ุฃูˆ VNC ุฃูˆ Telnet ู…ุจุงุดุฑุฉ. ุฃุฑุณู„ ุฑุงุจุทู‹ุง ูŠู…ูƒู† ู„ุฃูŠ ุดุฎุต ุงู„ุงู†ุถู…ุงู… ุฅู„ูŠู‡ ุฏูˆู† ุญุณุงุจุŒ ุฃูˆ ุดุงุฑูƒ ู…ุน ู…ุณุชุฎุฏู… Termix ู…ุญุฏุฏุŒ ู„ู„ู‚ุฑุงุกุฉ ูู‚ุท ุฃูˆ ู…ุน ุตู„ุงุญูŠุฉ ุงู„ูƒุชุงุจุฉ. ูŠู…ูƒู† ุฃู† ุชู†ุชู‡ูŠ ุงู„ู…ุดุงุฑูƒุงุช ุชู„ู‚ุงุฆูŠู‹ุง ุฃูˆ ุชูู„ุบู‰ ููŠ ุฃูŠ ูˆู‚ุชุŒ ูˆูŠู…ูƒู† ุฅูŠู‚ุงูู‡ุง ูƒู„ูŠู‹ุง ุฃูˆ ู„ูƒู„ ู…ุถูŠู ุนู„ู‰ ุญุฏุฉ. -**ุนู„ุงู…ุงุช ุงู„ุชุจูˆูŠุจ ุงู„ุฏุงุฆู…ุฉ:** -ุชุจู‚ู‰ ุฌู„ุณุงุช SSH ูˆุนู„ุงู…ุงุช ุงู„ุชุจูˆูŠุจ ู…ูุชูˆุญุฉ ุนุจุฑ ุงู„ุฃุฌู‡ุฒุฉ/ุงู„ุชุญุฏูŠุซุงุช ุฅุฐุง ุชู… ุชูุนูŠู„ู‡ุง ููŠ ู…ู„ู ุชุนุฑูŠู ุงู„ู…ุณุชุฎุฏู…. +**ุชุณุฌูŠู„ ุงู„ุฌู„ุณุงุช ูˆุงู„ุณุฌู„ุงุช:** +ุณุฌู‘ู„ ุฌู„ุณุงุช ุงู„ุทุฑููŠุฉ ูˆRDP ูˆVNC ูˆุดุงู‡ุฏู‡ุง ู„ุงุญู‚ู‹ุง. ู†ุฒู‘ู„ ุณุฌู„ุงุช ู†ุตูŠุฉ ู„ู„ุฌู„ุณุฉุŒ ูˆุงุทู‘ู„ุน ุนู„ู‰ ุณุฌู„ ุงู„ุงุชุตุงู„ ู„ุชุฑู‰ ุจุงู„ุถุจุท ู…ุง ุฌุฑู‰ ุฃุซู†ุงุก ุงู„ุงุชุตุงู„. + + + + +**ุงู„ุงุชุตุงู„ุงุช ุงู„ุชุณู„ุณู„ูŠุฉ:** +ุชูˆุงุตู„ ู…ุน ุงู„ุฃุฌู‡ุฒุฉ ุงู„ุชุณู„ุณู„ูŠุฉ ู…ุซู„ ุงู„ู…ูˆุฌู‘ู‡ุงุช ูˆุงู„ู…ุจุฏู‘ู„ุงุช ูˆุงู„ู…ุชุญูƒู…ุงุช ุงู„ุฏู‚ูŠู‚ุฉ ู…ู† ุงู„ู…ุชุตูุญ ุฃูˆ ุชุทุจูŠู‚ ุณุทุญ ุงู„ู…ูƒุชุจ. ุงุถุจุท ู…ุนุฏู„ ุงู„ุจุงูˆุฏ ูˆุจุชุงุช ุงู„ุจูŠุงู†ุงุช ูˆุจุชุงุช ุงู„ุชูˆู‚ู ูˆุงู„ุชู…ุงุซู„. ูŠุณุชุฎุฏู… ูˆุงุฌู‡ุฉ Web Serial ููŠ ุงู„ู…ุชุตูุญุงุช ุงู„ู…ุฏุนูˆู…ุฉุŒ ุฃูˆ ุฎู„ููŠุฉ ุฃุตู„ูŠุฉ ููŠ ุชุทุจูŠู‚ ุณุทุญ ุงู„ู…ูƒุชุจ. + + + + + + +**Tailscale:** +ุงุณุญุจ ุงู„ุฃุฌู‡ุฒุฉ ู…ู† ุดุจูƒุฉ tailnet ู„ุฅุถุงูุชู‡ุง ูƒู…ุถูŠูุงุช ุจุจุถุน ู†ู‚ุฑุงุชุŒ ูˆุงุชุตู„ ุนุจุฑ Tailscale SSH ู„ุชุชูˆู„ู‰ ู‚ูˆุงุฆู… ุตู„ุงุญูŠุงุช tailnet ุฃู…ุฑ ุงู„ูˆุตูˆู„ ุฏูˆู† ุชุฎุฒูŠู† ุจูŠุงู†ุงุช ุฏุฎูˆู„. ูŠุนู…ู„ ุฃูŠุถู‹ุง ู…ุน Headscale ูˆู†ู‚ุงุท ุงู„ูˆุตูˆู„ ุงู„ู…ุฎุตุตุฉ. + + + + +**Proxmox:** +ุงุณุชูˆุฑุฏ ุงู„ู…ุถูŠูุงุช ู…ุจุงุดุฑุฉ ู…ู† ู†ุณุฎุฉ ProxmoxุŒ ูˆุชุงุจุน ุฅุญุตุงุกุงุช ุงู„ุนู‚ุฏ ูˆุงู„ุฃู†ุธู…ุฉ ุงู„ุถูŠูุฉุŒ ุจู…ุง ููŠู‡ุง ุงู„ู…ุนุงู„ุฌ ูˆุงู„ุฐุงูƒุฑุฉ ูˆุงู„ุชุฎุฒูŠู†ุŒ ููŠ ุชุจูˆูŠุจ ุฎุงุต ุจู‡ุง. + + + + + + +**ู…ุณุงุญุงุช ุงู„ุนู…ู„ ูˆุนู„ุงู…ุงุช ุงู„ุชุจูˆูŠุจ:** +ุงุญูุธ ู…ุฌู…ูˆุนุฉ ู…ู† ุนู„ุงู…ุงุช ุงู„ุชุจูˆูŠุจ ุจุชู‚ุณูŠู…ู‡ุงุŒ ูˆุฃุนุฏ ูุชุญู‡ุง ูƒู„ู‡ุง ุจู†ู‚ุฑุฉ ูˆุงุญุฏุฉ. ูŠุชุฐูƒุฑ Termix ุฃูŠุถู‹ุง ุฌู„ุณุชูƒ ุงู„ุฃุฎูŠุฑุฉุŒ ูุชุนูˆุฏ ุนู„ุงู…ุงุช ุงู„ุชุจูˆูŠุจ ุจุนุฏ ุงู„ุชุญุฏูŠุซ ูˆุนู„ู‰ ุงู„ุฃุฌู‡ุฒุฉ ุงู„ุฃุฎุฑู‰. + + + + +**ุฅุนุฏุงุฏ ู…ูˆุฌูŽู‘ู‡:** +ุฅุนุฏุงุฏ ู‚ุตูŠุฑ ูŠุฑุดุฏูƒ ุฅู„ู‰ ุงุฎุชูŠุงุฑ ู†ู…ุท ุงู„ูˆุงุฌู‡ุฉ ูˆุงู„ุณู…ุฉ ูˆุงู„ู…ูŠุฒุงุช ุงู„ุชูŠ ุชุฑูŠุฏู‡ุง ูˆุฃูˆู„ ู…ุถูŠู ู„ูƒ. ูŠุฎููŠ ุงู„ูˆุถุน ุงู„ุจุณูŠุท ู…ุง ู„ุง ุชุณุชุฎุฏู…ู‡ุŒ ูˆูŠู…ูƒู†ูƒ ุฅุนุงุฏุฉ ุงู„ุฅุนุฏุงุฏ ุฃูˆ ุชุบูŠูŠุฑ ุงู„ู†ู…ุท ููŠ ุฃูŠ ูˆู‚ุช. + + + + + + +**ุชุทุจูŠู‚ ุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ู…ุณุชู‚ู„ ูˆุงู„ู…ุฒุงู…ู†ุฉ:** +ูŠุนู…ู„ ุชุทุจูŠู‚ ุณุทุญ ุงู„ู…ูƒุชุจ ุจู…ูุฑุฏู‡ ุจุฎู„ููŠุฉ ูˆู‚ุงุนุฏุฉ ุจูŠุงู†ุงุช ู…ุญู„ูŠุฉุŒ ุฏูˆู† ุญุงุฌุฉ ุฅู„ู‰ ุฎุงุฏู…. ูŠู…ูƒู†ูƒ ุฃูŠุถู‹ุง ุฑุจุทู‡ ุจุฎุงุฏู… Termix ู„ู…ุฒุงู…ู†ุฉ ุงู„ู…ุถูŠูุงุช ูˆุจูŠุงู†ุงุช ุงู„ุฏุฎูˆู„ ูˆุงู„ู…ู‚ุชุทูุงุช ูˆุบูŠุฑู‡ุง ููŠ ุงู„ุงุชุฌุงู‡ูŠู†ุŒ ูˆุงุฎุชูŠุงุฑ ู…ุง ุฅุฐุง ูƒุงู†ุช ุงู„ุงุชุตุงู„ุงุช ุชุจุฏุฃ ู…ู† ุฌู‡ุงุฒูƒ ุฃู… ุนุจุฑ ุงู„ุฎุงุฏู…. + + + + +**ุณุทุฑ ุงู„ุฃูˆุงู…ุฑ:** +ุฃุฏุงุฉ `termix` ู„ุณุทุฑ ุงู„ุฃูˆุงู…ุฑ ุชุนู…ู„ ููŠ ุงู„ุทุฑููŠุฉ ูˆููŠ ุณูƒุฑุจุชุงุชูƒ. ุงูุชุญ ุงู„ุทุฑููŠุงุชุŒ ูˆู†ูู‘ุฐ ุฃู…ุฑู‹ุง ุนู„ู‰ ู…ุถูŠู ูˆุงุญุฏ ุฃูˆ ุนู„ู‰ ุฃุณุทูˆู„ ูƒุงู…ู„ุŒ ูˆุงู†ู‚ู„ ุงู„ู…ู„ูุงุช ุนุจุฑ SFTPุŒ ูˆุฃุฏุฑ ุงู„ู…ุถูŠูุงุช ูˆุงู„ู…ู‚ุชุทูุงุช ูˆุจูŠุงู†ุงุช ุงู„ุฏุฎูˆู„. ุซุจู‘ุชู‡ุง ุนุจุฑ `npm install -g @termix-cli/cli` ุฃูˆ ุงุณุชุฎุฏู… ู…ู„ูู‹ุง ุชู†ููŠุฐูŠู‹ุง ู…ุณุชู‚ู„ู‹ุง. ุฑุงุฌุน [ูˆุซุงุฆู‚ ุณุทุฑ ุงู„ุฃูˆุงู…ุฑ](https://docs.termix.site/cli). + + + + + + +**ุงู„ุฃู…ุงู†:** +ุชูุดููŽู‘ุฑ ูƒู„ู…ุงุช ุงู„ู…ุฑูˆุฑ ูˆุงู„ู…ูุงุชูŠุญ ูˆุงู„ุฃุณุฑุงุฑ ุงู„ุฃุฎุฑู‰ ู„ูƒู„ ู…ุณุชุฎุฏู… ุนู„ู‰ ุญุฏุฉุŒ ูˆูŠู…ูƒู† ุชุดููŠุฑ ู…ู„ูุงุช ู‚ุงุนุฏุฉ ุงู„ุจูŠุงู†ุงุช ู†ูุณู‡ุง ุนู„ู‰ ุงู„ู‚ุฑุต. ุฑุงุฌุน [ุงู„ูˆุซุงุฆู‚](https://docs.termix.site/security) ู„ู…ุนุฑูุฉ ุขู„ูŠุฉ ุงู„ุนู…ู„. **ุงู„ู„ุบุงุช:** -ุฏุนู… ู…ุฏู…ุฌ ู„ุญูˆุงู„ูŠ 30 ู„ุบุฉ (ุชูุฏุงุฑ ุจูˆุงุณุทุฉ [Crowdin](https://docs.termix.site/translations)). +ู†ุญูˆ 30 ู„ุบุฉ ู…ุฏู…ุฌุฉุŒ ุชูุฏุงุฑ ุนุจุฑ [Crowdin](https://docs.termix.site/translations). @@ -195,40 +251,43 @@ Termix ู‡ูŠ ู…ู†ุตุฉ ู…ูุชูˆุญุฉ ุงู„ู…ุตุฏุฑ ูˆู…ุฌุงู†ูŠุฉ ู„ู„ุฃุจุฏ ูˆุฐุง
-

-ุงู„ู…ุฒูŠุฏ ู…ู† ุงู„ู…ูŠุฒุงุช +
+ู…ูŠุฒุงุช ุฃุฎุฑู‰
-- **ู„ูˆุญุฉ ุงู„ุชุญูƒู…** - ุนุฑุถ ู…ุนู„ูˆู…ุงุช ุงู„ุฎุงุฏู… ุจู†ุธุฑุฉ ูˆุงุญุฏุฉ ุนู„ู‰ ู„ูˆุญุฉ ุงู„ุชุญูƒู… -- **ู…ูุงุชูŠุญ API** - ุฅู†ุดุงุก ู…ูุงุชูŠุญ API ู…ุญุฏุฏุฉ ุงู„ู†ุทุงู‚ ู„ู„ู…ุณุชุฎุฏู… ู…ุน ุชูˆุงุฑูŠุฎ ุงู†ุชู‡ุงุก ุตู„ุงุญูŠุฉ ู„ู„ุงุณุชุฎุฏุงู… ููŠ ุงู„ุฃุชู…ุชุฉ/CI -- **ุชุตุฏูŠุฑ/ุงุณุชูŠุฑุงุฏ ุงู„ุจูŠุงู†ุงุช** - ุชุตุฏูŠุฑ ูˆุงุณุชูŠุฑุงุฏ ู…ุถูŠูุงุช SSH ูˆุจูŠุงู†ุงุช ุงู„ุงุนุชู…ุงุฏ ูˆุจูŠุงู†ุงุช ู…ุฏูŠุฑ ุงู„ู…ู„ูุงุช -- **ุฅุนุฏุงุฏ SSL ุชู„ู‚ุงุฆูŠ** - ุฅู†ุดุงุก ูˆุฅุฏุงุฑุฉ ุดู‡ุงุฏุงุช SSL ู…ุฏู…ุฌุฉ ู…ุน ุฅุนุงุฏุฉ ุงู„ุชูˆุฌูŠู‡ ุฅู„ู‰ HTTPS -- **ูˆุงุฌู‡ุฉ ู…ุณุชุฎุฏู… ุญุฏูŠุซุฉ** - ูˆุงุฌู‡ุฉ ู†ุธูŠูุฉ ู…ุชูˆุงูู‚ุฉ ู…ุน ุณุทุญ ุงู„ู…ูƒุชุจ ูˆุงู„ู‡ุงุชู ุงู„ู…ุญู…ูˆู„ ู…ุจู†ูŠุฉ ุจู€ React ูˆ Tailwind CSS ูˆ Shadcn. ุงู„ุงุฎุชูŠุงุฑ ุจูŠู† ุงู„ุนุฏูŠุฏ ู…ู† ุณู…ุงุช ูˆุงุฌู‡ุฉ ุงู„ู…ุณุชุฎุฏู… ุจู…ุง ููŠ ุฐู„ูƒ ุงู„ูุงุชุญ ูˆุงู„ุฏุงูƒู† ูˆ Dracula ูˆุบูŠุฑู‡ุง. ุงุณุชุฎุฏุงู… ู…ุณุงุฑุงุช URL ู„ูุชุญ ุฃูŠ ุงุชุตุงู„ ููŠ ูˆุถุน ู…ู„ุก ุงู„ุดุงุดุฉ. -- **ุณุฌู„ ุงู„ุฃูˆุงู…ุฑ** - ุงู„ุฅูƒู…ุงู„ ุงู„ุชู„ู‚ุงุฆูŠ ูˆุนุฑุถ ุฃูˆุงู…ุฑ SSH ุงู„ุชูŠ ุชู… ุชู†ููŠุฐู‡ุง ุณุงุจู‚ุงู‹ -- **ุงู„ุงุชุตุงู„ ุงู„ุณุฑูŠุน** - ุงู„ุงุชุตุงู„ ุจุฎุงุฏู… ุฏูˆู† ุงู„ุญุงุฌุฉ ุฅู„ู‰ ุญูุธ ุจูŠุงู†ุงุช ุงู„ุงุชุตุงู„ -- **ู„ูˆุญุฉ ุงู„ุฃูˆุงู…ุฑ** - ุงุถุบุท ู…ุฑุชูŠู† ุนู„ู‰ Shift ุงู„ุฃูŠุณุฑ ู„ู„ูˆุตูˆู„ ุงู„ุณุฑูŠุน ุฅู„ู‰ ุงุชุตุงู„ุงุช SSH ุจุงุณุชุฎุฏุงู… ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ -- **ุชูƒุงู…ู„ Proxmox** - ุฅุถุงูุฉ ุงู„ู…ุถูŠูุงุช ุชู„ู‚ุงุฆูŠุงู‹ ุฅู„ู‰ Termix ู…ู† ู†ุณุฎุฉ Proxmox ุงู„ุฎุงุตุฉ ุจูƒ -- **ู…ูŠุฒุงุช SSH ุงู„ุบู†ูŠุฉ** - ุฏุนู… ู…ุถูŠูุงุช ุงู„ู‚ูุฒุŒ WarpgateุŒ ุงู„ุงุชุตุงู„ุงุช ุงู„ู…ุจู†ูŠุฉ ุนู„ู‰ TOTPุŒ SOCKS5ุŒ ุงู„ุชุญู‚ู‚ ู…ู† ู…ูุชุงุญ ุงู„ู…ุถูŠูุŒ ุงู„ู…ู„ุก ุงู„ุชู„ู‚ุงุฆูŠ ู„ูƒู„ู…ุฉ ุงู„ู…ุฑูˆุฑุŒ [OPKSSH](https://github.com/openpubkey/opkssh)ุŒ tmuxุŒ port knockingุŒ ุชุณุฌูŠู„ ุงู„ุทุฑููŠุฉุŒ ุฅุนุงุฏุฉ ุชูˆุฌูŠู‡ ูˆูƒูŠู„ SSHุŒ ูˆูƒูŠู„ Bitwarden SSHุŒ ุชูˆู‚ูŠุน SSH ุนุจุฑ HashiCorp VaultุŒ ูˆุบูŠุฑู‡ุง. -- **Termix ID** - ู…ูƒุงูุฆ ู„ู€ sshid.io ู…ุฏู…ุฌ ููŠ Termix. ุงุญุตู„ ุนู„ู‰ ุงุณู… ู…ุณุชุฎุฏู…ุŒ ุงู†ุดุฑ ู…ูุงุชูŠุญ SSH ุงู„ุนุงู…ุฉ ุงู„ุฎุงุตุฉ ุจูƒ ุนู„ู‰ ุฑุงุจุท ู…ุญู„ู„ (resolver URL)ุŒ ูˆุงุณุชุฎุฏู… ู‡ูŠุฆุฉ ุฅุตุฏุงุฑ ุดู‡ุงุฏุงุช (CA) ู…ุฏู…ุฌุฉ ู„ุฅุตุฏุงุฑ ุดู‡ุงุฏุงุช SSH. +- **ู„ูˆุญุฉ ุงู„ู…ุนู„ูˆู…ุงุช** - ุฎูˆุงุฏู…ูƒ ููŠ ู„ู…ุญุฉ ูˆุงุญุฏุฉุŒ ุจุจุทุงู‚ุงุช ุชุฑุชู‘ุจู‡ุง ุจู†ูุณูƒ +- **ุฑุณู… ุงู„ุดุจูƒุฉ** - ู…ุฎุชุจุฑูƒ ุงู„ู…ู†ุฒู„ูŠ ู…ุฑุณูˆู…ู‹ุง ุงู†ุทู„ุงู‚ู‹ุง ู…ู† ู…ุถูŠูุงุชูƒุŒ ู…ุน ุงู„ุญุงู„ุฉ ู„ุญุธูŠู‹ุง +- **ู…ุฑุงู‚ุจ tmux** - ุชุตูุญ ุฌู„ุณุงุช tmux ูˆู†ูˆุงูุฐู‡ ูˆู„ูˆุญุงุชู‡ุŒ ู…ุน ู…ุนุงูŠู†ุฉ ูˆุจุญุซ +- **ู…ูุงุชูŠุญ API** - ู…ูุงุชูŠุญ ุฎุงุตุฉ ุจูƒู„ ู…ุณุชุฎุฏู… ู„ู‡ุง ุชุงุฑูŠุฎ ุงู†ุชู‡ุงุกุŒ ู„ู„ุณูƒุฑุจุชุงุช ูˆุฃู†ุธู…ุฉ CI +- **ุงู„ุชุตุฏูŠุฑ ูˆุงู„ุงุณุชูŠุฑุงุฏ** - ุงู†ู‚ู„ ุงู„ู…ุถูŠูุงุช ูˆุจูŠุงู†ุงุช ุงู„ุฏุฎูˆู„ ูˆุจูŠุงู†ุงุช ู…ุฏูŠุฑ ุงู„ู…ู„ูุงุช ุฅู„ู‰ ุงู„ุฏุงุฎู„ ูˆุงู„ุฎุงุฑุฌ +- **SSL ุชู„ู‚ุงุฆูŠ** - ุชูู†ุดุฃ ุงู„ุดู‡ุงุฏุงุช ูˆุชูุฌุฏูŽู‘ุฏ ู†ูŠุงุจุฉ ุนู†ูƒุŒ ู…ุน ุฅุนุงุฏุฉ ุงู„ุชูˆุฌูŠู‡ ุฅู„ู‰ HTTPSุŒ ุฃูˆ ุงุณุชุฎุฏู… ุดู‡ุงุฏุงุชูƒ ุงู„ุฎุงุตุฉ +- **ู‚ูˆุงุนุฏ ุงู„ุจูŠุงู†ุงุช** - SQLite ุงูุชุฑุงุถูŠู‹ุงุŒ ู…ุน ุฏุนู… PostgreSQL ูˆMySQL ุฃูŠุถู‹ุง +- **ูˆุงุฌู‡ุฉ ุญุฏูŠุซุฉ** - ูˆุงุฌู‡ุฉ React ุฃู†ูŠู‚ุฉ ุชุนู…ู„ ุนู„ู‰ ุณุทุญ ุงู„ู…ูƒุชุจ ูˆุงู„ู‡ุงุชูุŒ ุจุณู…ุงุช ู…ุซู„ ุงู„ูุงุชุญ ูˆุงู„ุฏุงูƒู† ูˆDracula. ูŠู…ูƒู† ูุชุญ ุฃูŠ ุงุชุตุงู„ ุจู…ู„ุก ุงู„ุดุงุดุฉ ู…ู† ุฑุงุจุท +- **ู„ูˆุญุฉ ุงู„ุฃูˆุงู…ุฑ** - ุงุถุบุท ู…ูุชุงุญ Shift ุงู„ุฃูŠุณุฑ ู…ุฑุชูŠู† ู„ู„ุงู†ุชู‚ุงู„ ุฅู„ู‰ ู…ุถูŠู ู…ู† ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ +- **ุงุฎุชุตุงุฑุงุช ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ** - ุงู„ุชู†ู‚ู„ ุจูŠู† ุนู„ุงู…ุงุช ุงู„ุชุจูˆูŠุจ ูˆุฅุบู„ุงู‚ู‡ุง ูˆุบูŠุฑ ุฐู„ูƒุŒ ูˆูƒู„ู‡ุง ู‚ุงุจู„ุฉ ู„ุฅุนุงุฏุฉ ุงู„ุชุนูŠูŠู† +- **Wake-on-LAN** - ุฃูŠู‚ุธ ุฌู‡ุงุฒู‹ุง ู…ู† Termix ุฃูˆ ู…ู† ุฎุทูˆุฉ ููŠ ุงู„ุฃุชู…ุชุฉ +- **ู…ุตุงุฏู‚ุฉ ุงู„ูˆูƒูŠู„ ุงู„ู…ูˆุซูˆู‚** - ุฏุน ูˆูƒูŠู„ู‹ุง ุนูƒุณูŠู‹ุง ูŠุชูˆู„ู‰ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ูˆูŠู…ุฑู‘ุฑ ุงู„ู…ุณุชุฎุฏู… +- **SSH ุจุฅู…ูƒุงู†ุงุช ูˆุงุณุนุฉ** - ู…ุถูŠูุงุช ูˆุณูŠุทุฉ ูˆWarpgate ูˆุทู„ุจุงุช TOTP ูˆSOCKS5 ูˆุงู„ุชุญู‚ู‚ ู…ู† ู…ูุงุชูŠุญ ุงู„ู…ุถูŠู ูˆุงู„ุชุนุจุฆุฉ ุงู„ุชู„ู‚ุงุฆูŠุฉ ู„ูƒู„ู…ุงุช ุงู„ู…ุฑูˆุฑ ูˆ[OPKSSH](https://github.com/openpubkey/opkssh) ูˆtmux ูˆport knocking ูˆุณุฌู„ุงุช ุงู„ุทุฑููŠุฉ ูˆุชู…ุฑูŠุฑ ุงู„ูˆูƒูŠู„ ูˆูˆูƒูŠู„ SSH ู…ู† Bitwarden ูˆุชูˆู‚ูŠุน SSH ุนุจุฑ HashiCorp Vault ูˆุบูŠุฑู‡ุง +- **Termix ID** - ู†ุณุฎุฉ ู…ุฏู…ุฌุฉ ุนู„ู‰ ุบุฑุงุฑ sshid.io. ุงุญุฌุฒ ู…ุนุฑู‘ูู‹ุงุŒ ูˆุงู†ุดุฑ ู…ูุงุชูŠุญูƒ ุงู„ุนุงู…ุฉ ุนู„ู‰ ุฑุงุจุท ู…ุญู„ูู‘ู„ุŒ ูˆุฃุตุฏุฑ ุดู‡ุงุฏุงุช SSH ู…ู† ุณู„ุทุฉ ุงู„ุชุตุฏูŠู‚ ุงู„ู…ุฏู…ุฌุฉ

-## ุฏุนู… ุงู„ู…ู†ุตุงุช +## ุงู„ู…ู†ุตุงุช ุงู„ู…ุฏุนูˆู…ุฉ - + - + - + @@ -252,9 +311,9 @@ Termix ู‡ูŠ ู…ู†ุตุฉ ู…ูุชูˆุญุฉ ุงู„ู…ุตุฏุฑ ูˆู…ุฌุงู†ูŠุฉ ู„ู„ุฃุจุฏ ูˆุฐุง ## ุงู„ุชุซุจูŠุช -ู‚ู… ุจุฒูŠุงุฑุฉ [ูˆุซุงุฆู‚](https://docs.termix.site/install) Termix ู„ู„ุญุตูˆู„ ุนู„ู‰ ุชุนู„ูŠู…ุงุช ุงู„ุชุซุจูŠุช ุงู„ูƒุงู…ู„ุฉ ุนุจุฑ ุฌู…ูŠุน ุงู„ู…ู†ุตุงุช. +ุฑุงุฌุน [ูˆุซุงุฆู‚ Termix](https://docs.termix.site/install) ู„ู„ุงุทู„ุงุน ุนู„ู‰ ุชุนู„ูŠู…ุงุช ุงู„ุชุซุจูŠุช ุงู„ูƒุงู…ู„ุฉ ู„ุฌู…ูŠุน ุงู„ู…ู†ุตุงุช. -ู†ู…ูˆุฐุฌ ู…ู„ู Docker Compose (ูŠู…ูƒู†ูƒ ุญุฐู `guacd` ูˆุงู„ุดุจูƒุฉ ุฅุฐุง ูƒู†ุช ู„ุง ุชุฎุทุท ู„ุงุณุชุฎุฏุงู… ู…ูŠุฒุงุช ุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏ): +ู…ุซุงู„ ุนู„ู‰ ู…ู„ู Docker Compose (ูŠู…ูƒู†ูƒ ุญุฐู `guacd` ูˆุงู„ุดุจูƒุฉ ุฅุฐุง ูƒู†ุช ู„ุง ุชู†ูˆูŠ ุงุณุชุฎุฏุงู… ุณุทุญ ุงู„ู…ูƒุชุจ ุงู„ุจุนูŠุฏ): ```yaml services: @@ -291,19 +350,45 @@ networks: driver: bridge ``` +### ุณุทุฑ ุงู„ุฃูˆุงู…ุฑ + +ูŠูˆูุฑ Termix ุฃูŠุถู‹ุง ุฃุฏุงุฉ ุณุทุฑ ุฃูˆุงู…ุฑุŒ ู„ุชุฏูŠุฑ ุฎูˆุงุฏู…ูƒ ู…ู† ุงู„ุทุฑููŠุฉ ูˆุชุณุชุฎุฏู… Termix ุฏุงุฎู„ ุณูƒุฑุจุชุงุชูƒ. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +ุชุณุชุทูŠุน ูุชุญ ุงู„ุทุฑููŠุงุชุŒ ูˆุชู†ููŠุฐ ุฃู…ุฑ ุนู„ู‰ ู…ุถูŠู ูˆุงุญุฏ ุฃูˆ ุนู„ู‰ ุฃุณุทูˆู„ ูƒุงู…ู„ุŒ ูˆู†ู‚ู„ ุงู„ู…ู„ูุงุช ุนุจุฑ SFTPุŒ ูˆุฅุฏุงุฑุฉ ุงู„ู…ุถูŠูุงุช ูˆุงู„ู…ู‚ุชุทูุงุช ูˆุจูŠุงู†ุงุช ุงู„ุฏุฎูˆู„. ุงู„ูˆุซุงุฆู‚ ุงู„ูƒุงู…ู„ุฉ ุนู„ู‰ [docs.termix.site/cli](https://docs.termix.site/cli). + +### ุงู„ุงุณุชุถุงูุฉ ุงู„ุณุญุงุจูŠุฉ + +ูŠู…ูƒู†ูƒ ุชุดุบูŠู„ ุฎุงุฏู… Termix ุนู„ู‰ VPS ุจุฏู„ู‹ุง ู…ู† ุฏุงุฎู„ ุดุจูƒุชูƒ. ุฅุฐุง ูƒุงู† Termix ูŠุนู…ู„ ุฏุงุฎู„ ุงู„ุดุจูƒุฉ ุงู„ุชูŠ ูŠุฏูŠุฑู‡ุงุŒ ูุฃูŠ ุนุทู„ ุณูŠุฃุฎุฐู‡ ู…ุนู‡ุŒ ุชุญุฏูŠุฏู‹ุง ุญูŠู† ุชุญุชุงุฌ ุฅู„ูŠู‡ ู„ุฅุตู„ุงุญ ุงู„ุฃู…ูˆุฑ. ุชุดุบูŠู„ู‡ ููŠ ุงู„ุฎุงุฑุฌ ูŠุจู‚ูŠู‡ ู…ุชุงุญู‹ุงุŒ ูˆูŠู…ู†ุญูƒ ุนู†ูˆุงู† IP ุซุงุจุชู‹ุงุŒ ูˆูŠุชูŠุญ ู„ูƒ ุงู„ุฏุฎูˆู„ ู…ู† ุฃูŠ ู…ูƒุงู† ุฏูˆู† VPN ุฃูˆ ูุชุญ ู…ู†ุงูุฐ. + +ุชุฑุนู‰ [GINERNET](https://docs.termix.site/install/ginernet) ู…ุดุฑูˆุน TermixุŒ ูˆุชุชุถู…ู† ุงู„ูˆุซุงุฆู‚ ุฏู„ูŠู„ู‹ุง ุฎุทูˆุฉ ุจุฎุทูˆุฉ ู„ู„ู†ุดุฑ ุนู„ู‰ ู…ู†ุตุฉ ุงู„ุฎูˆุงุฏู… ุงู„ุงูุชุฑุงุถูŠุฉ ุงู„ุฎุงุตุฉ ุจู‡ู…. + +
+ +## ุจูŠุงู†ุงุช ุงู„ุงุณุชุฎุฏุงู… + +ูŠุฑุณู„ Termix ุฅุดุงุฑุฉ ุตุบูŠุฑุฉ ู…ุฌู‡ูˆู„ุฉ ู…ุฑุฉ ูˆุงุญุฏุฉ ูŠูˆู…ูŠู‹ุงุŒ ู„ุฃุนุฑู ุนุฏุฏ ุงู„ู†ุณุฎ ุงู„ุนุงู…ู„ุฉ ูˆุงู„ู…ูŠุฒุงุช ุงู„ู…ุณุชุฎุฏู…ุฉ ูุนู„ู‹ุง. ุชุญุชูˆูŠ ุนู„ู‰ ู…ุนุฑู‘ู ุนุดูˆุงุฆูŠ ู„ู„ู†ุณุฎุฉุŒ ูˆุนุฏุฏ ุงู„ู…ุณุชุฎุฏู…ูŠู† ูˆุงู„ู…ุถูŠูุงุช ู„ุฏูŠูƒุŒ ูˆุฅุตุฏุงุฑ ุงู„ุชุทุจูŠู‚ุŒ ูˆุงู„ู…ูŠุฒุงุช ุงู„ุชูŠ ุงุณุชูุฎุฏู…ุช ุฎู„ุงู„ ุขุฎุฑ 24 ุณุงุนุฉ (ุงู„ุทุฑููŠุฉ ูˆู…ุฏูŠุฑ ุงู„ู…ู„ูุงุช ูˆุงู„ุฃู†ูุงู‚ ูˆdocker ูˆุบูŠุฑู‡ุง). ูˆู„ุง ุชุญุชูˆูŠ ุฃุจุฏู‹ุง ุนู„ู‰ ุฃุณู…ุงุก ู…ุณุชุฎุฏู…ูŠู† ุฃูˆ ุฃุณู…ุงุก ู…ุถูŠูุงุช ุฃูˆ ุนู†ุงูˆูŠู† IP ุฃูˆ ุจูŠุงู†ุงุช ุฏุฎูˆู„ ุฃูˆ ุฃูŠ ุดูŠุก ูŠุนุฑู‘ู ุจูƒ ุฃูˆ ุจุฎูˆุงุฏู…ูƒ. + +ูˆู‡ูŠ ู…ูุนู‘ู„ุฉ ุงูุชุฑุงุถูŠู‹ุง. ูŠู…ูƒู†ูƒ ุฅูŠู‚ุงูู‡ุง ู…ู† ุฅุนุฏุงุฏุงุช ุงู„ู…ุณุคูˆู„ ุถู…ู† ู‚ุณู… ุนุงู…ุŒ ุฃูˆ ุถุจุท `ENABLE_TELEMETRY=false` ู‚ุจู„ ุชุดุบูŠู„ Termix ุฃุตู„ู‹ุง. +
## ุงู„ุชุจุฑุน -Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ุจุฏูˆู† ุงุดุชุฑุงูƒุงุช ุฃูˆ ุฎุทุท ู…ุฏููˆุนุฉ. ุฅุฐุง ูˆุฌุฏุชู‡ ู…ููŠุฏู‹ุงุŒ ููƒู‘ุฑ ููŠ ุงู„ุชุจุฑุน ู„ู„ู…ุณุงุนุฏุฉ ููŠ ุชุบุทูŠุฉ ุชูƒุงู„ูŠู ุงู„ุฎุงุฏู… ูˆุงู„ู†ุทุงู‚ุงุช ูˆูˆู‚ุช ุงู„ุชุทูˆูŠุฑ. ุชุณุงุนุฏ ุงู„ุชุจุฑุนุงุช ุฃูŠุถุงู‹ ููŠ ุชู…ูˆูŠู„ ุงู„ูˆู‚ุช ุงู„ู„ุงุฒู… ู„ู„ุจุญุซ ูˆุชุนู„ู… ู…ุง ู‡ูˆ ู…ุทู„ูˆุจ ู„ุจู†ุงุก ู…ูŠุฒุงุช ู…ุซู„ SAML ูˆ Kubernetes ูˆุฏุนู… ุงู„ูˆูƒู„ุงุก (Agent). ุชุงุจุน ุงู„ุชู‚ุฏู… ูˆุชุจุฑุน ุฃุฏู†ุงู‡. +Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑุŒ ุจู„ุง ุงุดุชุฑุงูƒุงุช ูˆู„ุง ุฎุทุท ู…ุฏููˆุนุฉ. ุฅุฐุง ูƒุงู† ู…ููŠุฏู‹ุง ู„ูƒุŒ ููƒู‘ุฑ ููŠ ุงู„ุชุจุฑุน ู„ู„ู…ุณุงุนุฏุฉ ููŠ ุชุบุทูŠุฉ ุงู„ุฎูˆุงุฏู… ูˆุงู„ู†ุทุงู‚ุงุช ูˆูˆู‚ุช ุงู„ุชุทูˆูŠุฑ. ุชู…ูˆู‘ู„ ุงู„ุชุจุฑุนุงุช ุฃูŠุถู‹ุง ูˆู‚ุช ุงู„ุจุญุซ ูˆุงู„ุชุนู„ู‘ู… ุงู„ู„ุงุฒู… ู„ุจู†ุงุก ู…ูŠุฒุงุช ู…ุซู„ SAML ูˆKubernetes ูˆุฏุนู… ุงู„ูˆูƒู„ุงุก. ุชุงุจุน ุงู„ุชู‚ุฏู… ูˆุชุจุฑุน ู…ู† ุงู„ุฑุงุจุท ุฃุฏู†ุงู‡. -[ุชุจุฑุน](https://donate.termix.site/) +[ุชุจุฑู‘ุน](https://donate.termix.site/)
## ุงู„ุฑุนุงุฉ -ู‡ู„ ุชุฑูŠุฏ ุฅุนู„ุงู†ุงู‹ ู…ุฏููˆุนุงู‹ ู„ุฏุนู… ุงู„ุชุทูˆูŠุฑุŸ ุฑุงุณู„ู†ุง ุนุจุฑ ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ [mail@termix.site](mailto:mail@termix.site). +ู‡ู„ ุชู‡ุชู… ุจู…ุณุงุญุฉ ุฅุนู„ุงู†ูŠุฉ ู…ุฏููˆุนุฉ ู„ุฏุนู… ุงู„ุชุทูˆูŠุฑุŸ ุฑุงุณู„ู†ุง ุนู„ู‰ [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ุจุฏูˆู† ุงุดุชุฑุงูƒุงุช ุฃูˆ ุฎุท Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ุจุฏูˆู† ุงุดุชุฑุงูƒุงุช ุฃูˆ ุฎุท Rack Genius - +    + + Ginernet +

## ุงู„ุฏุนู… -ุฅุฐุง ูƒู†ุช ุจุญุงุฌุฉ ุฅู„ู‰ ู…ุณุงุนุฏุฉ ุฃูˆ ุชุฑุบุจ ููŠ ุทู„ุจ ู…ูŠุฒุฉ ู„ู€ TermixุŒ ู‚ู… ุจุฒูŠุงุฑุฉ ุตูุญุฉ [ุงู„ู…ุดูƒู„ุงุช](https://github.com/Termix-SSH/Support/issues)ุŒ ูˆุณุฌู„ ุงู„ุฏุฎูˆู„ุŒ ูˆุงุถุบุท ุนู„ู‰ `New Issue`. ูŠุฑุฌู‰ ุฃู† ุชูƒูˆู† ู…ูุตู„ุงู‹ ู‚ุฏุฑ ุงู„ุฅู…ูƒุงู† ููŠ ู…ุดูƒู„ุชูƒุŒ ูˆูŠููุถูŽู‘ู„ ูƒุชุงุจุชู‡ุง ุจุงู„ู„ุบุฉ ุงู„ุฅู†ุฌู„ูŠุฒูŠุฉ. ูŠู…ูƒู†ูƒ ุฃูŠุถุงู‹ ุงู„ุงู†ุถู…ุงู… ุฅู„ู‰ ุฎุงุฏู… [Discord](https://discord.gg/jVQGdvHDrf) ูˆุฒูŠุงุฑุฉ ู‚ู†ุงุฉ ุงู„ุฏุนู…ุŒ ูˆู…ุน ุฐู„ูƒ ู‚ุฏ ุชูƒูˆู† ุฃูˆู‚ุงุช ุงู„ุงุณุชุฌุงุจุฉ ุฃุทูˆู„. +ุชุญุชุงุฌ ู…ุณุงุนุฏุฉ ุฃูˆ ุชุฑูŠุฏ ุทู„ุจ ู…ูŠุฒุฉุŸ ุงูุชุญ [ู…ุดูƒู„ุฉ ุฌุฏูŠุฏุฉ](https://github.com/Termix-SSH/Support/issues) ูˆุงุฐูƒุฑ ุฃูƒุจุฑ ู‚ุฏุฑ ู…ู…ูƒู† ู…ู† ุงู„ุชูุงุตูŠู„ุŒ ุจุงู„ุฅู†ุฌู„ูŠุฒูŠุฉ ุฅู† ุฃู…ูƒู†. ูŠู…ูƒู†ูƒ ุฃูŠุถู‹ุง ุงู„ุณุคุงู„ ููŠ ู‚ู†ุงุฉ ุงู„ุฏุนู… ุนู„ู‰ [Discord](https://discord.gg/jVQGdvHDrf)ุŒ ูˆุฅู† ูƒุงู†ุช ุงู„ุฑุฏูˆุฏ ู‡ู†ุงูƒ ู‚ุฏ ุชุชุฃุฎุฑ.
@@ -359,7 +443,7 @@ Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ุจุฏูˆู† ุงุดุชุฑุงูƒุงุช ุฃูˆ ุฎุท [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -ุดุงู‡ุฏ ู†ุธุฑุฉ ุนุงู…ุฉ ุนู„ู‰ ุงู„ุชุญุฏูŠุซุงุช ุนู„ู‰ YouTube +ุดุงู‡ุฏ ุนุฑูˆุถ ุงู„ุชุญุฏูŠุซุงุช ุนู„ู‰ YouTube

@@ -399,7 +483,7 @@ Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ุจุฏูˆู† ุงุดุชุฑุงูƒุงุช ุฃูˆ ุฎุท
ุงู„ู…ู†ุตุฉุงู„ุชูˆุฒูŠุนุทุฑูŠู‚ุฉ ุงู„ุชูˆุฒูŠุน
WebุฃูŠ ู…ุชุตูุญ ุญุฏูŠุซ (ChromeุŒ SafariุŒ Firefox) ยท ุฏุนู… PWAุฃูŠ ู…ุชุตูุญ ุญุฏูŠุซ (Chrome ูˆSafari ูˆFirefox) ยท ูŠุฏุนู… PWA
Windows x64/ia32ู†ุณุฎุฉ ู…ุญู…ูˆู„ุฉ ยท ู…ุซุจุช MSI ยท Chocolateyู†ุณุฎุฉ ู…ุญู…ูˆู„ุฉ ยท ู…ุซุจู‘ุช MSI ยท Chocolatey
Linux x64/ia32
-ู‚ุฏ ุชูƒูˆู† ุจุนุถ ู…ู‚ุงุทุน ุงู„ููŠุฏูŠูˆ ูˆุงู„ุตูˆุฑ ู‚ุฏูŠู…ุฉ ุฃูˆ ู‚ุฏ ู„ุง ุชุนุฑุถ ุงู„ู…ูŠุฒุงุช ุจุดูƒู„ ู…ุซุงู„ูŠ. +ู‚ุฏ ุชูƒูˆู† ุจุนุถ ุงู„ู…ู‚ุงุทุน ูˆุงู„ุตูˆุฑ ู‚ุฏูŠู…ุฉ ุฃูˆ ู„ุง ุชุนุฑุถ ุงู„ู…ูŠุฒุงุช ุนู„ู‰ ุฃูุถู„ ูˆุฌู‡. @@ -407,10 +491,10 @@ Termix ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ุจุฏูˆู† ุงุดุชุฑุงูƒุงุช ุฃูˆ ุฎุท ## ุงู„ู…ูŠุฒุงุช ุงู„ู…ุฎุทุทุฉ -ุฑุงุฌุน [ุงู„ู…ุดุงุฑูŠุน](https://github.com/orgs/Termix-SSH/projects/5) ู„ุนุฑุถ ุฌู…ูŠุน ุงู„ู…ูŠุฒุงุช ุงู„ู…ุฎุทุทุฉ. ุฅุฐุง ูƒู†ุช ุชุชุทู„ุน ู„ู„ู…ุณุงู‡ู…ุฉุŒ ุฑุงุฌุน [ุงู„ู…ุณุงู‡ู…ุฉ](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +ุฌู…ูŠุน ุงู„ู…ูŠุฒุงุช ุงู„ู…ุฎุทุทุฉ ู…ูˆุฌูˆุฏุฉ ููŠ [Projects](https://github.com/orgs/Termix-SSH/projects/5). ุฅุฐุง ุฃุฑุฏุช ุงู„ู…ุณุงู‡ู…ุฉุŒ ุฑุงุฌุน [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
## ุงู„ุชุฑุฎูŠุต -ู…ูˆุฒุน ุจู…ูˆุฌุจ ุฑุฎุตุฉ Apache License ุงู„ุฅุตุฏุงุฑ 2.0. ุฑุงุฌุน ู…ู„ู `LICENSE` ู„ู…ุฒูŠุฏ ู…ู† ุงู„ู…ุนู„ูˆู…ุงุช. +ูŠูˆุฒูŽู‘ุน ุจู…ูˆุฌุจ ุชุฑุฎูŠุต Apache ุงู„ุฅุตุฏุงุฑ 2.0. ุฑุงุฌุน ู…ู„ู `LICENSE` ู„ู…ุฒูŠุฏ ู…ู† ุงู„ู…ุนู„ูˆู…ุงุช. diff --git a/docs/readme/README-CN.md b/docs/readme/README-CN.md index 2290b99..b189343 100644 --- a/docs/readme/README-CN.md +++ b/docs/readme/README-CN.md @@ -4,7 +4,7 @@

Termix

-

่‡ชๆ‰˜็ฎก SSH ็ฎก็†ไธŽ่ฟœ็จ‹ๆกŒ้ข่ฎฟ้—ฎๅนณๅฐ

+

่‡ชๆ‰˜็ฎกๆœๅŠกๅ™จ็ฎก็†๏ผŒไปŽ SSH ๅ’Œ่ฟœ็จ‹ๆกŒ้ขๅˆฐ่‡ชๅŠจๅŒ–

English ยท @@ -58,7 +58,7 @@ Termix ๅ…่ดนไธ”ๅผ€ๆบใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ๏ผŒ่ฏท่€ƒ่™‘[ๆ่ต ](https://do ## ๆฆ‚่งˆ -Termix ๆ˜ฏไธ€ไธชๅผ€ๆบใ€ๆฐธไน…ๅ…่ดนใ€่‡ชๆ‰˜็ฎก็š„ไธ€ไฝ“ๅŒ–ๆœๅŠกๅ™จ็ฎก็†ๅนณๅฐใ€‚ๅฎƒๆไพ›ไบ†ไธ€ไธชๅคšๅนณๅฐ่งฃๅ†ณๆ–นๆกˆ๏ผŒ้€š่ฟ‡ไธ€ไธช็›ด่ง‚็š„็•Œ้ข็ฎก็†ไฝ ็š„ๆœๅŠกๅ™จๅ’ŒๅŸบ็ก€่ฎพๆ–ฝใ€‚Termix ๆไพ› SSH ็ปˆ็ซฏ่ฎฟ้—ฎใ€่ฟœ็จ‹ๆกŒ้ขๆŽงๅˆถ๏ผˆRDPใ€VNCใ€Telnet๏ผ‰ใ€SSH ้šง้“ๅŠŸ่ƒฝใ€่ฟœ็จ‹ๆ–‡ไปถ็ฎก็†ไปฅๅŠ่ฎธๅคšๅ…ถไป–ๅทฅๅ…ทใ€‚Termix ๆ˜ฏ้€‚็”จไบŽๆ‰€ๆœ‰ๅนณๅฐ็š„ๅฎŒ็พŽๅ…่ดน่‡ชๆ‰˜็ฎก Termius ๆ›ฟไปฃๅ“ใ€‚ +Termix ๆ˜ฏไธ€ไธชๅ…่ดนใ€ๅผ€ๆบใ€่‡ชๆ‰˜็ฎก็š„ๆœๅŠกๅ™จ็ฎก็†ๅนณๅฐใ€‚ๅฎƒๆŠŠ SSH ็ปˆ็ซฏใ€่ฟœ็จ‹ๆกŒ้ข๏ผˆRDPใ€VNCใ€Telnet๏ผ‰ใ€ๆ–‡ไปถไผ ่พ“ใ€้šง้“ใ€Dockerใ€ๆŒ‡ๆ ‡ๅ’Œ่‡ชๅŠจๅŒ–้›†ไธญๅœจไธ€ไธชๅœฐๆ–น๏ผŒๆ”ฏๆŒ็ฝ‘้กต็ซฏใ€ๆกŒ้ข็ซฏๅ’Œ็งปๅŠจ็ซฏใ€‚ๅฎƒๆ˜ฏ Termius ็š„่‡ชๆ‰˜็ฎกๆ›ฟไปฃๅ“๏ผŒๅนถไธ”ๆฐธไน…ๅ…่ดนใ€‚
@@ -68,42 +68,42 @@ Termix ๆ˜ฏไธ€ไธชๅผ€ๆบใ€ๆฐธไน…ๅ…่ดนใ€่‡ชๆ‰˜็ฎก็š„ไธ€ไฝ“ๅŒ–ๆœๅŠกๅ™จ็ฎก็†ๅนณ -**SSH ็ปˆ็ซฏ่ฎฟ้—ฎ:** -ๅŠŸ่ƒฝ้ฝๅ…จ็š„็ปˆ็ซฏ๏ผŒๆ”ฏๆŒๅˆ†ๅฑ๏ผˆๆœ€ๅคš 4 ไธช้ขๆฟ๏ผ‰๏ผŒๅนถ้…ๆœ‰็ฑปไผผๆต่งˆๅ™จ็š„ๆ ‡็ญพ็ณป็ปŸใ€‚ๅŒ…ๆ‹ฌๅฏน่‡ชๅฎšไน‰็ปˆ็ซฏ็š„ๆ”ฏๆŒ๏ผŒๅฆ‚ๅธธ็”จ็š„็ปˆ็ซฏไธป้ข˜ใ€ๅญ—ไฝ“ๅ’Œๅ…ถไป–็ป„ไปถใ€‚ +**SSH ็ปˆ็ซฏ:** +ๅŠŸ่ƒฝ้ฝๅ…จ็š„็ปˆ็ซฏ๏ผŒ้…ๆœ‰็ฑปไผผๆต่งˆๅ™จ็š„ๆ ‡็ญพ้กตๅ’Œๅˆ†ๅฑ๏ผŒๆœ€ๅคšๅŒๆ—ถๆ˜พ็คบ 6 ไธช้ขๆฟใ€‚ๅฏไปฅ้€‰ๆ‹ฉไธป้ข˜ใ€ๅญ—ไฝ“ๅ’Œ้…่‰ฒใ€‚ๆฏไธชไผš่ฏไธŠๆ–น้ƒฝๆœ‰ไธ€ไธชๅทฅๅ…ทๆ ๏ผŒๆ˜พ็คบๅฎžๆ—ถ็š„ CPUใ€ๅ†…ๅญ˜ๅ’Œ็ฃ็›˜๏ผŒๅนถๆไพ›ๆŒ‡ๅ‘่ฏฅไธปๆœบๆ–‡ไปถใ€Dockerใ€้šง้“ๅ’ŒๆŒ‡ๆ ‡็š„ๅฟซๆทๅ…ฅๅฃใ€‚ -**่ฟœ็จ‹ๆกŒ้ข่ฎฟ้—ฎ:** -้€š่ฟ‡ๆต่งˆๅ™จๆ”ฏๆŒ RDPใ€VNC ๅ’Œ Telnet๏ผŒๅ…ทๆœ‰ๅฎŒๆ•ด็š„่‡ชๅฎšไน‰ๅ’Œๅˆ†ๅฑๅŠŸ่ƒฝใ€‚ +**่ฟœ็จ‹ๆกŒ้ข:** +ๅœจๆต่งˆๅ™จไธญไฝฟ็”จ RDPใ€VNC ๅ’Œ Telnet๏ผŒๅ’Œๅ…ถไป–ไผš่ฏไธ€ๆ ทๆ”ฏๆŒๆ ‡็ญพ้กตๅ’Œๅˆ†ๅฑใ€‚ๅŒ…ๅซ RDP ้ฉฑๅŠจๅ™จ็š„ๆ–‡ไปถๆต่งˆๅ™จๅ’Œๆ‹–ๆ”พไธŠไผ ใ€‚ๅœจ Windows ๆกŒ้ข็ซฏ๏ผŒไฝ ่ฟ˜ๅฏไปฅ็”จๅŽŸ็”Ÿ RDP ๅฎขๆˆท็ซฏๆ‰“ๅผ€ไธปๆœบใ€‚ -**SSH ้šง้“็ฎก็†:** -ๅˆ›ๅปบๅ’Œ็ฎก็†ๅ…ทๆœ‰่‡ชๅŠจ้‡่ฟžๅ’Œๅฅๅบท็›‘ๆต‹ๅŠŸ่ƒฝ็š„ๆœๅŠกๅ™จ้—ด SSH ้šง้“๏ผŒๆ”ฏๆŒๆœฌๅœฐใ€่ฟœ็จ‹ๆˆ–ๅŠจๆ€ SOCKS ่ฝฌๅ‘ใ€‚ๆกŒ้ขๅฎขๆˆท็ซฏๅˆฐๆœๅŠกๅ™จ็š„้šง้“่ฎพ็ฝฎๆŒ‰ๆกŒ้ขๅฎ‰่ฃ…ๆœฌๅœฐๅญ˜ๅ‚จ๏ผŒๅฏ้€‰็š„ C2S ้ข„่ฎพๅฟซ็…งๅฏไฟๅญ˜ๅˆฐๆœๅŠกๅ™จใ€้‡ๅ‘ฝๅใ€ๅŠ ่ฝฝๆˆ–ๅˆ ้™ค๏ผŒไปฅไพฟๅœจๅฎขๆˆท็ซฏไน‹้—ด่ฟ็งปๆœฌๅœฐ้šง้“้…็ฝฎใ€‚ +**SSH ้šง้“:** +ๆ”ฏๆŒๆœฌๅœฐใ€่ฟœ็จ‹ๅ’ŒๅŠจๆ€ SOCKS ่ฝฌๅ‘๏ผŒๅฏ่‡ชๅŠจ้‡่ฟžๅนถ่ฟ›่กŒๅฅๅบทๆฃ€ๆŸฅใ€‚ๆกŒ้ข็ซฏ็š„ๅฎขๆˆท็ซฏๅˆฐๆœๅŠกๅ™จ้šง้“ไฟๅญ˜ๅœจๆœฌๆœบ๏ผŒไฝ ไนŸๅฏไปฅๆŠŠ้ข„่ฎพไฟๅญ˜ๅˆฐๆœๅŠกๅ™จ๏ผŒไปฅไพฟ่ฟ็งปๅˆฐๅฆไธ€ๅฐๅฎขๆˆท็ซฏใ€‚ -**่ฟœ็จ‹ๆ–‡ไปถ็ฎก็†ๅ™จ:** -็›ดๆŽฅๅœจ่ฟœ็จ‹ๆœๅŠกๅ™จไธŠ็ฎก็†ๆ–‡ไปถ๏ผŒๆ”ฏๆŒๆŸฅ็œ‹ๅ’Œ็ผ–่พ‘ไปฃ็ ใ€ๅ›พๅƒใ€้Ÿณ้ข‘ๅ’Œ่ง†้ข‘ใ€‚ๆ”ฏๆŒ้€š่ฟ‡ sudo ๆ— ็ผไธŠไผ ใ€ไธ‹่ฝฝใ€้‡ๅ‘ฝๅใ€ๅˆ ้™คๅ’Œ็งปๅŠจๆ–‡ไปถใ€‚ๅŒ…ๆ‹ฌๆ”ฏๆŒๅœจๆœๅŠกๅ™จไน‹้—ด็งปๅŠจๆ–‡ไปถใ€‚ +**ๆ–‡ไปถ็ฎก็†ๅ™จ:** +้€š่ฟ‡ SFTP ๆต่งˆใ€็ผ–่พ‘ใ€ไธŠไผ ใ€ไธ‹่ฝฝใ€้‡ๅ‘ฝๅใ€็งปๅŠจๅ’Œๅˆ ้™คๆ–‡ไปถ๏ผŒๆ”ฏๆŒ sudoใ€‚ๅฏไปฅๆŸฅ็œ‹ๅ’Œ็ผ–่พ‘ไปฃ็ ใ€ๅ›พ็‰‡ใ€้Ÿณ้ข‘ๅ’Œ่ง†้ข‘ใ€‚ๆ–‡ไปถๅฏไปฅ็›ดๆŽฅไปŽไธ€ๅฐๆœๅŠกๅ™จๅคๅˆถๅˆฐๅฆไธ€ๅฐ๏ผŒ็ณป็ปŸไผš่‡ชๅŠจ้€‰ๆ‹ฉๆœ€ๅฟซ็š„่ทฏๅพ„ๅนถๆ ก้ชŒไผ ่พ“ๅฎŒๆ•ดๆ€งใ€‚ -**Docker ๅ’Œ Podman ็ฎก็†:** -ๅฏๅŠจใ€ๅœๆญขใ€ๆš‚ๅœใ€็งป้™คๅฎนๅ™จใ€‚ๆŸฅ็œ‹ๅฎนๅ™จ็ปŸ่ฎกไฟกๆฏใ€‚้€š่ฟ‡ docker exec ็ปˆ็ซฏๆŽงๅˆถๅฎนๅ™จใ€‚ๅŒๆ—ถๆ”ฏๆŒ Docker ๅ’Œ Podman ไฝœไธบๅฎนๅ™จ่ฟ่กŒๆ—ถใ€‚ๅฎƒ็š„ๅˆ่กทไธๆ˜ฏๅ–ไปฃ Portainer ๆˆ– Dockge๏ผŒ่€Œๆ˜ฏไธบไบ†ๆฏ”็›ดๆŽฅๅˆ›ๅปบๅฎนๅ™จๆ›ด็ฎ€ๅ•ๅœฐ็ฎก็†ๅฎƒไปฌใ€‚ +**Docker ๅ’Œ Podman:** +ๅฏๅŠจใ€ๅœๆญขใ€ๆš‚ๅœๅ’Œๅˆ ้™คๅฎนๅ™จ๏ผŒๆŸฅ็œ‹ๅฎƒไปฌ็š„็Šถๆ€๏ผŒๅนถๅœจๅฎนๅ™จๅ†…ๆ‰“ๅผ€ไธ€ไธช็ปˆ็ซฏใ€‚ๅŒๆ—ถๆ”ฏๆŒ Docker ๅ’Œ Podmanใ€‚ๅฎƒไธๆ˜ฏ่ฆๅ–ไปฃ Portainer ๆˆ– Dockge๏ผŒๅชๆ˜ฏ็”จๆฅ็ฎก็†ไฝ ๅทฒๆœ‰็š„ๅฎนๅ™จใ€‚ -**SSH ไธปๆœบ็ฎก็†ๅ™จ:** -้€š่ฟ‡ๆ ‡็ญพๅ’Œๆ–‡ไปถๅคน๏ผˆๆ”ฏๆŒๆ–‡ไปถๅคน่‡ชๅฎšไน‰ๅ’ŒๅตŒๅฅ—ๆ–‡ไปถๅคน๏ผ‰ไฟๅญ˜ใ€็ป„็ป‡ๅ’Œ็ฎก็†ๆ‚จ็š„ SSH ่ฟžๆŽฅ๏ผŒ่ฝปๆพไฟๅญ˜ๅฏ้‡็”จ็š„็™ปๅฝ•ไฟกๆฏ๏ผŒๅนถ่ƒฝ่‡ชๅŠจๅŒ–้ƒจ็ฝฒ SSH ๅฏ†้’ฅใ€‚ +**ไธปๆœบ็ฎก็†:** +็”จๆ ‡็ญพๅ’Œๅฏๅ‘ฝๅใ€ๅฏ้…่‰ฒ็š„ๅตŒๅฅ—ๆ–‡ไปถๅคนๆฅๆ•ด็†ไธปๆœบใ€‚ๅœจๅคšๅฐไธปๆœบไน‹้—ดๅค็”จๅทฒไฟๅญ˜็š„ๅ‡ญๆฎ๏ผŒ่‡ชๅŠจ้ƒจ็ฝฒ SSH ๅฏ†้’ฅ๏ผŒๆŠŠไธปๆœบๅฝ’ๅˆฐ็ˆถไธปๆœบไธ‹๏ผŒๆ‰น้‡็ผ–่พ‘ๅ’Œๅฏผๅ‡บ๏ผŒ่ฟ˜ๅฏไปฅ็”จๅฟซ้€Ÿ่ฟžๆŽฅๅค„็†้‚ฃไบ›ไธๆƒณไฟๅญ˜็š„ไธ€ๆฌกๆ€ง่ฟžๆŽฅใ€‚ @@ -111,83 +111,139 @@ Termix ๆ˜ฏไธ€ไธชๅผ€ๆบใ€ๆฐธไน…ๅ…่ดนใ€่‡ชๆ‰˜็ฎก็š„ไธ€ไฝ“ๅŒ–ๆœๅŠกๅ™จ็ฎก็†ๅนณ **ไธปๆœบๆŒ‡ๆ ‡:** -ๅœจๅคงๅคšๆ•ฐๅŸบไบŽ Linux ็š„ๆœๅŠกๅ™จไธŠๆŸฅ็œ‹ CPUใ€ๅ†…ๅญ˜ใ€็ฃ็›˜ไฝฟ็”จๆƒ…ๅ†ตใ€็ฝ‘็ปœใ€่ฟ่กŒๆ—ถ้—ดใ€็ณป็ปŸไฟกๆฏใ€้˜ฒ็ซๅข™ใ€็ซฏๅฃ็›‘ๆŽงใ€ๆ—ฅๅฟ—ๆŸฅ็œ‹ๅ™จใ€็”จๆˆท/ๆƒ้™ใ€่ฏไนฆ็ญ‰ๆ›ดๅคšไฟกๆฏใ€‚ๅŒ…ๆ‹ฌๆ—ถ้—ดๅบๅˆ—ๅކๅฒๅ›พ่กจๅ’Œๆ”ฏๆŒ ntfy ไธŽ webhook ็š„้˜ˆๅ€ผๅ‘Š่ญฆใ€‚ +ๅœจๅคงๅคšๆ•ฐ Linux ๆœๅŠกๅ™จไธŠๆŸฅ็œ‹ CPUใ€ๅ†…ๅญ˜ใ€็ฃ็›˜ใ€็ฝ‘็ปœใ€ๆธฉๅบฆใ€่ฟ่กŒๆ—ถ้—ดใ€่ฟ›็จ‹ใ€็ซฏๅฃใ€็™ปๅฝ•่ฎฐๅฝ•ๅ’Œ็ณป็ปŸไฟกๆฏ๏ผŒๅนถ้™„ๅธฆๅކๅฒๆ›ฒ็บฟๅ›พใ€‚็ฎก็†ๅก็‰‡่ฎฉไฝ ๆ— ้œ€็ฆปๅผ€ Termix ๅฐฑ่ƒฝๅค„็†ๆœๅŠกใ€ๅฎšๆ—ถไปปๅŠกใ€่ฝฏไปถๅŒ…ใ€็”จๆˆทใ€้˜ฒ็ซๅข™่ง„ๅˆ™ใ€WireGuardใ€Tailscaleใ€SSL ่ฏไนฆใ€ๆ—ฅๅฟ—ๅ’Œๅฅๅบทๆฃ€ๆŸฅใ€‚ -**็”จๆˆท่ฎค่ฏ:** -ๅฎ‰ๅ…จ็š„็”จๆˆท็ฎก็†๏ผŒๅ…ทๆœ‰็ฎก็†ๅ‘˜ๆŽงๅˆถ๏ผˆๅฏ็ผ–่พ‘ๅ…ถไป–็”จๆˆทไฟกๆฏ๏ผ‰ๅ’Œ OIDC/LDAP/SSO๏ผˆๅธฆ่ฎฟ้—ฎๆŽงๅˆถ๏ผ‰ใ€2FA (TOTP) ไปฅๅŠ้€š่กŒๅฏ†้’ฅ๏ผˆWebAuthn๏ผ‰ๆ”ฏๆŒใ€‚ๆŸฅ็œ‹ๆ‰€ๆœ‰ๅนณๅฐไธŠ็š„ๆดปๅŠจ็”จๆˆทไผš่ฏๅนถๆ’ค้”€ๆƒ้™ใ€‚ๅฐ†ๆ‚จ็š„ OIDC/ๆœฌๅœฐ่ดฆๆˆท้“พๆŽฅๅœจไธ€่ตทใ€‚ๆŸฅ็œ‹ๆ‰€ๆœ‰็”จๆˆทๆ“ไฝœ็š„ๅฎก่ฎกๆ—ฅๅฟ—ใ€‚ +**่‡ชๅŠจๅŒ–:** +ๅ…ˆ้€‰ไธ€ไธช่งฆๅ‘ๆกไปถ๏ผŒๅ†ๅ†ณๅฎš่ฆๅšไป€ไนˆใ€‚่งฆๅ‘ๆกไปถๅŒ…ๆ‹ฌๆŒ‡ๆ ‡่ถ…่ฟ‡้˜ˆๅ€ผใ€ไธปๆœบไธŠ็บฟๆˆ–ไธ‹็บฟใ€ๅฅๅบทๆฃ€ๆŸฅ็Šถๆ€ๅ˜ๅŒ–ใ€ๅฎšๆ—ถ่ฎกๅˆ’ใ€ๅฎนๅ™จไบ‹ไปถ๏ผŒๆˆ–่€…ไธ€ไธชไผ ๅ…ฅ็š„ Webhookใ€‚ๆญฅ้ชคๅฏไปฅๆ‰ง่กŒๅ‘ฝไปคๅ’Œไปฃ็ ็‰‡ๆฎตใ€ๆŽงๅˆถๅฎนๅ™จๅ’Œ้šง้“ใ€ๅ”ค้†’ไธปๆœบใ€่ฐƒ็”จๆŸไธช็ฝ‘ๅ€ใ€็ญ‰ๅพ…ใ€ๆŒ‰ๆกไปถๅˆ†ๆ”ฏใ€่ฟ่กŒๅฆไธ€ไธช่‡ชๅŠจๅŒ–๏ผŒๅนถ้€š่ฟ‡ ntfyใ€Discord ๆˆ– Webhook ้€š็Ÿฅไฝ ใ€‚ๆต‹่ฏ•่ฟ่กŒ่ฎฉไฝ ๅ…ˆๅฎ‰ๅ…จๅœฐ่ฏ•ไธ€้ใ€‚ -**Tailscale ้›†ๆˆ:** -ๅˆ—ๅ‡บๆ‚จ Tailscale ็ฝ‘็ปœไธญ็š„่ฎพๅค‡ไปฅๅฟซ้€ŸๆทปๅŠ ไธบไธปๆœบ๏ผŒๅนถไฝฟ็”จ Tailscale SSH ไฝœไธบ่บซไปฝ้ชŒ่ฏๆ–นๅผ๏ผŒ่ฎฉๆ‚จ็š„ Tailscale ACL ๅค„็†ๆŽˆๆƒ่€Œๆ— ้œ€ๅญ˜ๅ‚จๅ‡ญๆฎใ€‚ +**ๆœบ็พค:** +้€š่ฟ‡ๆ‰‹ๅŠจๆŒ‘้€‰ๆˆ–ๆ ‡็ญพ่ง„ๅˆ™ๆŠŠไธปๆœบ็ผ–ๆˆไธ€ไธชๆœบ็พค๏ผŒๆ–ฐไธปๆœบๅฏไปฅ่‡ชๅŠจๅŠ ๅ…ฅใ€‚ไธ€ๆฌกๅœจๆ‰€ๆœ‰ไธปๆœบไธŠๆ‰ง่กŒๅŒไธ€ๆกๅ‘ฝไปค๏ผŒๅ‘ๅ…จ้ƒจไธปๆœบๆŽจ้€ๅ’Œๆ‹‰ๅ–ๆ–‡ไปถ๏ผŒๅฎ‰่ฃ…่ฝฏไปถๅŒ…๏ผŒๅนถๆ”ถ้›†็ณป็ปŸใ€ๅ†…ๆ ธใ€ๆžถๆž„ๅ’Œ่ฟ่กŒๆ—ถ้—ด็š„ๆธ…ๅ•ใ€‚ -**RBAC/ๅ…ฑไบซ:** -ๅˆ›ๅปบ่ง’่‰ฒๅนถๅœจ็”จๆˆท/่ง’่‰ฒไน‹้—ดๅ…ฑไบซไธปๆœบใ€‚ๆ”ฏๆŒๆ‰€ๆœ‰่ฎค่ฏ็ฑปๅž‹ๅ’Œๆ‰€ๆœ‰ไธปๆœบๅ่ฎฎใ€‚ +**AI ๅŠฉๆ‰‹:** +ๅฏ้€‰ๅŠŸ่ƒฝ๏ผŒ้ป˜่ฎคๅ…ณ้—ญ๏ผŒ้œ€่ฆไฝ ๆ‰‹ๅŠจๅผ€ๅฏใ€‚ๆŽฅๅ…ฅ OpenAIใ€Anthropicใ€Geminiใ€Ollama ๆˆ–ไปปไฝ•ๅ…ผๅฎน OpenAI ็š„ๆŽฅๅฃ๏ผŒๅ‘ๅฎƒ่ฏข้—ฎไฝ ็š„้…็ฝฎใ€‚ๅฎƒๅฏไปฅ่ฏปๅ–ไธปๆœบใ€ๆœบ็พคใ€ไปฃ็ ็‰‡ๆฎตๅ’Œๅ‘Š่ญฆ๏ผŒๅนถๆŠŠๆ”นๅŠจไฝœไธบๅปบ่ฎฎๆไบค็ป™ไฝ ็กฎ่ฎค๏ผŒ่€Œไธไผš่‡ช่กŒไฟฎๆ”นใ€‚ๅฎƒๆฐธ่ฟœๆ— ๆณ•ๆŽฅ่งฆๅ‡ญๆฎใ€็”จๆˆทๅ’Œ่ฎพ็ฝฎใ€‚็ฎก็†ๅ‘˜ๅฏไปฅๅฏนๆ•ดไธชๅฎžไพ‹ๅ…ณ้—ญๅฎƒ๏ผŒไฝ ไนŸๅฏไปฅๅœจๅˆๅง‹่ฎพ็ฝฎๆ—ถๆŠŠๅฎƒ้š่—ใ€‚ -**ไธฒๅฃ่ฟžๆŽฅ:** -็›ดๆŽฅไปŽๆต่งˆๅ™จๆˆ–ๆกŒ้ขๅบ”็”จ่ฟžๆŽฅๅˆฐไธฒๅฃ่ฎพๅค‡๏ผˆ่ทฏ็”ฑๅ™จใ€ไบคๆขๆœบใ€ๅพฎๆŽงๅˆถๅ™จ็ญ‰๏ผ‰ใ€‚้…็ฝฎๆณข็‰น็އใ€ๆ•ฐๆฎไฝใ€ๅœๆญขไฝๅ’Œๅฅ‡ๅถๆ ก้ชŒใ€‚ๅœจๆ”ฏๆŒ็š„ๆต่งˆๅ™จไธญไฝฟ็”จ Web Serial API๏ผŒๆˆ–ๅœจ Electron ๅบ”็”จไธญไฝฟ็”จๅŽŸ็”ŸๅŽ็ซฏใ€‚ +**็™ปๅฝ•ไธŽ็”จๆˆท:** +ๆ”ฏๆŒๆœฌๅœฐ่ดฆๆˆท๏ผŒไปฅๅŠ OIDCใ€LDAPใ€GitHub ๅ’Œ Google ็™ปๅฝ•๏ผŒ่ฟ˜ๆœ‰ไธคๆญฅ้ชŒ่ฏ๏ผˆTOTP๏ผ‰ใ€้€š่กŒๅฏ†้’ฅ๏ผˆWebAuthn๏ผ‰ๅ’Œๅ—ไฟกไปป่ฎพๅค‡ใ€‚็ฎก็†ๅ‘˜ๅฏไปฅ็ฎก็†็”จๆˆทใ€ๆŠŠ OIDC ็พค็ป„ๆ˜ ๅฐ„ๅˆฐ่ง’่‰ฒใ€ๆŸฅ็œ‹ๆ‰€ๆœ‰ๅนณๅฐไธŠ็š„ๆดปๅŠจไผš่ฏๅนถๅฐ†ๅ…ถๅŠ้”€ใ€‚ไฝ ๅฏไปฅๆŠŠๆœฌๅœฐ่ดฆๆˆทๅ’Œ OIDC ่ดฆๆˆทๅ…ณ่”่ตทๆฅ๏ผŒๅนถๆŸฅ็œ‹่ฎฐๅฝ•ๆ‰€ๆœ‰ไบบๆ“ไฝœ็š„ๅฎก่ฎกๆ—ฅๅฟ—ใ€‚ +**่ง’่‰ฒไธŽๅ…ฑไบซ:** +ๅˆ›ๅปบ่ง’่‰ฒ๏ผŒๅนถๆŒ‰ๅ››ไธช็บงๅˆซๆŠŠไธปๆœบๅ…ฑไบซ็ป™็”จๆˆทๆˆ–่ง’่‰ฒ๏ผš่ฟžๆŽฅใ€ๆŸฅ็œ‹ใ€็ผ–่พ‘ๅ’Œ็ฎก็†ใ€‚้€‚็”จไบŽๆ‰€ๆœ‰่ฎค่ฏๆ–นๅผๅ’Œๆ‰€ๆœ‰ๅ่ฎฎ๏ผŒๅนถไธ”ๅฏไปฅ่ฆ†็›–ๅ…ฑไบซไธปๆœบๆ‰€ไฝฟ็”จ็š„ๅ‡ญๆฎใ€‚ + + + + + + **ๅ‘Š่ญฆ:** -ไธบไธปๆœบๆŒ‡ๆ ‡๏ผˆCPUใ€ๅ†…ๅญ˜ใ€็ฃ็›˜็ญ‰๏ผ‰่ฎพ็ฝฎๅŸบไบŽ้˜ˆๅ€ผ็š„ๅ‘Š่ญฆ่ง„ๅˆ™๏ผŒๅนถ้€š่ฟ‡ ntfy ๆˆ– webhook ๆŽฅๆ”ถ่งฆๅ‘้€š็Ÿฅใ€‚ๅœจๅކๅฒๆ—ฅๅฟ—ไธญๆŸฅ็œ‹่งฆๅ‘ๅ’Œๅทฒ่งฃๅ†ณ็š„ๅ‘Š่ญฆใ€‚ +ไธบ CPUใ€ๅ†…ๅญ˜ใ€็ฃ็›˜็ญ‰ไธปๆœบๆŒ‡ๆ ‡่ฎพ็ฝฎ่ง„ๅˆ™๏ผŒ่งฆๅ‘ๆ—ถ้€š่ฟ‡ ntfyใ€Discord ๆˆ– Webhook ้€š็Ÿฅไฝ ใ€‚ๅœจๅކๅฒ่ฎฐๅฝ•ไธญๆŸฅ็œ‹ๆญฃๅœจ่งฆๅ‘ๅ’Œๅทฒๆขๅค็š„ๅ‘Š่ญฆ๏ผŒๅนถๅฟฝ็•ฅไฝ ไธๅ…ณๅฟƒ็š„้‚ฃไบ›ใ€‚ - - **ไธป้กต:** -ๅ…ทๆœ‰ๆ‹–ๆ”พๅฐ็ป„ไปถ็ฝ‘ๆ ผ็š„ๅฎŒๅ…จๅฏๅฎšๅˆถไธป้กตใ€‚ๆทปๅŠ ไธปๆœบ็Šถๆ€ใ€ๆœๅŠก้“พๆŽฅใ€ๆ—ถ้’Ÿใ€็ฌ”่ฎฐใ€RSS ่ฎข้˜…ใ€ๅคฉๆฐ”ใ€Docker ๅฎนๅ™จใ€ไธปๆœบๆŒ‡ๆ ‡ๅ›พ่กจใ€ๅตŒๅ…ฅๅผ็ปˆ็ซฏใ€iframe ็ญ‰ๅฐ็ป„ไปถใ€‚ - - - - -**ๆ•ฐๆฎๅบ“ๅŠ ๅฏ†:** -ๅŽ็ซฏๅญ˜ๅ‚จไธบๅŠ ๅฏ†็š„ SQLite ๆ•ฐๆฎๅบ“ๆ–‡ไปถใ€‚ๆŸฅ็œ‹[ๆ–‡ๆกฃ](https://docs.termix.site/security)ไบ†่งฃๆ›ดๅคšใ€‚ +ไธ€ไธช็”ฑไฝ ่‡ชๅทฑๆญๅปบ็š„ๆ‹–ๆ”พๅฐ็ป„ไปถ็ฝ‘ๆ ผใ€‚ๅฐ็ป„ไปถๅŒ…ๆ‹ฌไธปๆœบ็Šถๆ€ใ€Pingใ€ๆœๅŠก้“พๆŽฅใ€ไนฆ็ญพใ€ๆœ็ดขใ€ๆ—ถ้’Ÿใ€ๆ—ฅๅކใ€ๅ€’่ฎกๆ—ถใ€ไพฟ็ญพใ€RSSใ€ๅคฉๆฐ”ใ€ๅ›พ็‰‡ใ€ๅ†…ๅตŒ็ฝ‘้กตใ€Dockerใ€้šง้“ใ€ๆŒ‡ๆ ‡ๅ›พ่กจใ€่‡ชๅฎšไน‰ API๏ผŒ็”š่‡ณ่ฟ˜ๆœ‰ไธ€ไธชๅฎžๆ—ถ็ปˆ็ซฏใ€‚ -**็ฝ‘็ปœๅ›พ:** -่‡ชๅฎšไน‰ๆ‚จ็š„ไปช่กจๆฟ๏ผŒๆ นๆฎๆ‚จ็š„ SSH ่ฟžๆŽฅๅฏ่ง†ๅŒ–ๆ‚จ็š„ๅฎถๅบญๅฎž้ชŒๅฎค๏ผŒๅนถๆ”ฏๆŒ็Šถๆ€็›‘ๆต‹ใ€‚ +**ไปฃ็ ็‰‡ๆฎตไธŽๅทฅๅ…ท:** +ไฟๅญ˜ๅธธ็”จๅ‘ฝไปค๏ผŒไธ€้”ฎๆ‰ง่กŒ๏ผŒๅนถๆ”ฏๆŒไธปๆœบๅ˜้‡ๅ’Œ่‡ชๅฎšไน‰่พ“ๅ…ฅใ€‚ๅฏไปฅๅœจๆ‰€ๆœ‰ๅทฒๆ‰“ๅผ€็š„็ปˆ็ซฏไธญๅŒๆ—ถ่ฟ่กŒไธ€ๆกๅ‘ฝไปค๏ผŒไนŸๅฏไปฅๅธฆ่‡ชๅŠจ่กฅๅ…จๅœฐๆœ็ดขๅ‘ฝไปคๅކๅฒใ€‚ -**SSH ๅทฅๅ…ท:** -ๅˆ›ๅปบๅฏ้‡็”จ็š„ๅ‘ฝไปค็‰‡ๆฎต๏ผŒๅช้œ€็‚นๅ‡ปไธ€ไธ‹ๅณๅฏๆ‰ง่กŒใ€‚ๅœจๅคšไธชๆ‰“ๅผ€็š„็ปˆ็ซฏไธญๅŒๆ—ถ่ฟ่กŒไธ€ไธชๅ‘ฝไปคใ€‚ +**ไผš่ฏๅ…ฑไบซ:** +ๅฎžๆ—ถๅ…ฑไบซ็ปˆ็ซฏใ€RDPใ€VNC ๆˆ– Telnet ไผš่ฏใ€‚ๅฏไปฅๅ‘้€ไธ€ไธชๆ— ้œ€่ดฆๆˆทๅณๅฏๅŠ ๅ…ฅ็š„้“พๆŽฅ๏ผŒไนŸๅฏไปฅๅ…ฑไบซ็ป™ๆŒ‡ๅฎš็š„ Termix ็”จๆˆท๏ผŒๅนถ้€‰ๆ‹ฉๅช่ฏปๆˆ–ๅฏ่ฏปๅ†™ใ€‚ๅ…ฑไบซๅฏไปฅ่‡ชๅŠจ่ฟ‡ๆœŸๆˆ–้šๆ—ถๆ’ค้”€๏ผŒไนŸๅฏไปฅๅ…จๅฑ€ๆˆ–ๆŒ‰ไธปๆœบๅ…ณ้—ญใ€‚ -**ๆŒไน…ๆ ‡็ญพ้กต:** -ๅฆ‚ๆžœๅœจ็”จๆˆทไธชไบบ่ต„ๆ–™ไธญๅฏ็”จ๏ผŒSSH ไผš่ฏๅ’Œๆ ‡็ญพ้กตๅฐ†ๅœจ่ฎพๅค‡/ๅˆทๆ–ฐๅŽไฟๆŒๆ‰“ๅผ€็Šถๆ€ใ€‚ +**ไผš่ฏๅฝ•ๅˆถไธŽๆ—ฅๅฟ—:** +ๅฝ•ๅˆถ็ปˆ็ซฏใ€RDP ๅ’Œ VNC ไผš่ฏ๏ผŒไน‹ๅŽๅฏไปฅๅ›žๆ”พใ€‚ๅฏไปฅไธ‹่ฝฝไผš่ฏ็š„็บฏๆ–‡ๆœฌๆ—ฅๅฟ—๏ผŒไนŸๅฏไปฅๆŸฅ็œ‹่ฟžๆŽฅๆ—ฅๅฟ—๏ผŒไบ†่งฃ่ฟžๆŽฅ่ฟ‡็จ‹ไธญ็ฉถ็ซŸๅ‘็”Ÿไบ†ไป€ไนˆใ€‚ -**่ฏญ่จ€:** -ๅ†…็ฝฎๆ”ฏๆŒ็บฆ 30 ็ง่ฏญ่จ€๏ผˆ็”ฑ [Crowdin](https://docs.termix.site/translations) ็ฎก็†๏ผ‰ใ€‚ +**ไธฒๅฃ่ฟžๆŽฅ:** +ไปŽๆต่งˆๅ™จๆˆ–ๆกŒ้ขๅบ”็”จ่ฟžๆŽฅ่ทฏ็”ฑๅ™จใ€ไบคๆขๆœบๅ’Œๅ•็‰‡ๆœบ็ญ‰ไธฒๅฃ่ฎพๅค‡ใ€‚ๅฏ่ฎพ็ฝฎๆณข็‰น็އใ€ๆ•ฐๆฎไฝใ€ๅœๆญขไฝๅ’Œๆ ก้ชŒไฝใ€‚ๅœจๆ”ฏๆŒ็š„ๆต่งˆๅ™จไธญไฝฟ็”จ Web Serial API๏ผŒๅœจๆกŒ้ขๅบ”็”จไธญไฝฟ็”จๅŽŸ็”ŸๅŽ็ซฏใ€‚ + + + + + + +**Tailscale:** +ไปŽไฝ ็š„ tailnet ไธญๆ‹‰ๅ–่ฎพๅค‡๏ผŒ็‚นๅ‡ ไธ‹ๅฐฑ่ƒฝๆŠŠๅฎƒไปฌๆทปๅŠ ไธบไธปๆœบ๏ผŒๅนถไฝฟ็”จ Tailscale SSH ่ฟžๆŽฅ๏ผŒ็”ฑ tailnet ACL ่ดŸ่ดฃ่ฎฟ้—ฎๆŽงๅˆถ๏ผŒๆ— ้œ€ไฟๅญ˜ไปปไฝ•ๅ‡ญๆฎใ€‚ไนŸๆ”ฏๆŒ Headscale ๅ’Œ่‡ชๅฎšไน‰ๆŽฅๅฃๅœฐๅ€ใ€‚ + + + + +**Proxmox:** +็›ดๆŽฅไปŽ Proxmox ๅฎžไพ‹ๅฏผๅ…ฅไธปๆœบ๏ผŒๅนถๅœจไธ“ๅฑžๆ ‡็ญพ้กตไธญๆŸฅ็œ‹่Š‚็‚นๅ’Œ่™šๆ‹Ÿๆœบ็š„็Šถๆ€๏ผŒๅŒ…ๆ‹ฌ CPUใ€ๅ†…ๅญ˜ๅ’Œๅญ˜ๅ‚จใ€‚ + + + + + + +**ๅทฅไฝœๅŒบไธŽๆ ‡็ญพ้กต:** +ไฟๅญ˜ไธ€็ป„ๆ ‡็ญพ้กตๅŠๅ…ถๅˆ†ๅฑๅธƒๅฑ€๏ผŒไธ€้”ฎๅฐฑ่ƒฝๆŠŠๆ•ดๅฅ—้‡ๆ–ฐๆ‰“ๅผ€ใ€‚Termix ่ฟ˜ไผš่ฎฐไฝไฝ ไธŠๆฌก็š„ไผš่ฏ๏ผŒๆ‰€ไปฅๅˆทๆ–ฐ้กต้ขๆˆ–ๆข่ฎพๅค‡ๅŽๆ ‡็ญพ้กต้ƒฝไผšๅ›žๆฅใ€‚ + + + + +**ๅผ•ๅฏผ่ฎพ็ฝฎ:** +ไธ€ไธช็ฎ€็Ÿญ็š„ๅผ•ๅฏผๆต็จ‹ไผšๅธฆไฝ ้€‰ๆ‹ฉ็•Œ้ข้ข„่ฎพใ€ไธป้ข˜ใ€้œ€่ฆ็š„ๅŠŸ่ƒฝ๏ผŒไปฅๅŠ็ฌฌไธ€ๅฐไธปๆœบใ€‚็ฎ€ๆดๆจกๅผไผš้š่—ไฝ ็”จไธๅˆฐ็š„ไธœ่ฅฟ๏ผŒไฝ ้šๆ—ถๅฏไปฅ้‡ๆ–ฐ่ฟ่กŒๅผ•ๅฏผๆˆ–ๅˆ‡ๆข้ข„่ฎพใ€‚ + + + + + + +**ๆกŒ้ข็‹ฌ็ซ‹่ฟ่กŒไธŽๅŒๆญฅ:** +ๆกŒ้ขๅบ”็”จๅฏไปฅๅฎŒๅ…จ็‹ฌ็ซ‹่ฟ่กŒ๏ผŒ่‡ชๅธฆๆœฌๅœฐๅŽ็ซฏๅ’Œๆ•ฐๆฎๅบ“๏ผŒไธ้œ€่ฆๆœๅŠกๅ™จใ€‚ไฝ ไนŸๅฏไปฅๆŠŠๅฎƒ่ฟžๅˆฐ Termix ๆœๅŠกๅ™จ๏ผŒๅŒๅ‘ๅŒๆญฅไธปๆœบใ€ๅ‡ญๆฎใ€ไปฃ็ ็‰‡ๆฎต็ญ‰ๅ†…ๅฎน๏ผŒๅนถ้€‰ๆ‹ฉ่ฟžๆŽฅๆ˜ฏๅœจๆœฌๅœฐๅ‘่ตท่ฟ˜ๆ˜ฏ้€š่ฟ‡ๆœๅŠกๅ™จๅ‘่ตทใ€‚ + + + + +**ๅ‘ฝไปค่กŒๅทฅๅ…ท:** +`termix` ๅ‘ฝไปค่กŒๅทฅๅ…ท๏ผŒๅฏ็”จไบŽไฝ ็š„็ปˆ็ซฏๅ’Œ่„šๆœฌใ€‚ๆ‰“ๅผ€็ปˆ็ซฏใ€ๅœจๅ•ๅฐไธปๆœบๆˆ–ๆ•ดไธชๆœบ็พคไธŠๆ‰ง่กŒๅ‘ฝไปคใ€้€š่ฟ‡ SFTP ไผ ่พ“ๆ–‡ไปถ๏ผŒไปฅๅŠ็ฎก็†ไธปๆœบใ€ไปฃ็ ็‰‡ๆฎตๅ’Œๅ‡ญๆฎใ€‚็”จ `npm install -g @termix-cli/cli` ๅฎ‰่ฃ…๏ผŒๆˆ–่€…็›ดๆŽฅไธ‹่ฝฝ็‹ฌ็ซ‹็š„ๅฏๆ‰ง่กŒๆ–‡ไปถใ€‚่ฏฆ่ง [CLI ๆ–‡ๆกฃ](https://docs.termix.site/cli)ใ€‚ + + + + + + +**ๅฎ‰ๅ…จ:** +ๅฏ†็ ใ€ๅฏ†้’ฅๅ’Œๅ…ถไป–ๆœบๅฏ†ๆŒ‰็”จๆˆทๅŠ ๅฏ†๏ผŒๆ•ฐๆฎๅบ“ๆ–‡ไปถๆœฌ่บซไนŸๅฏไปฅๅœจ็ฃ็›˜ไธŠๅŠ ๅฏ†ใ€‚ๅ…ทไฝ“ๅŽŸ็†่ฏทๆŸฅ็œ‹[ๆ–‡ๆกฃ](https://docs.termix.site/security)ใ€‚ + + + + +**ๅคš่ฏญ่จ€:** +ๅ†…็ฝฎ็บฆ 30 ็ง่ฏญ่จ€๏ผŒ้€š่ฟ‡ [Crowdin](https://docs.termix.site/translations) ็ฎก็†ใ€‚ @@ -199,17 +255,20 @@ Termix ๆ˜ฏไธ€ไธชๅผ€ๆบใ€ๆฐธไน…ๅ…่ดนใ€่‡ชๆ‰˜็ฎก็š„ไธ€ไฝ“ๅŒ–ๆœๅŠกๅ™จ็ฎก็†ๅนณ

ๆ›ดๅคšๅŠŸ่ƒฝ
-- **ไปช่กจๆฟ** - ๅœจไปช่กจๆฟไธŠไธ€็›ฎไบ†็„ถๅœฐๆŸฅ็œ‹ๆœๅŠกๅ™จไฟกๆฏ -- **API ๅฏ†้’ฅ** - ๅˆ›ๅปบๅธฆๆœ‰ๅˆฐๆœŸๆ—ฅๆœŸ็š„็”จๆˆท่Œƒๅ›ด API ๅฏ†้’ฅ๏ผŒ็”จไบŽ่‡ชๅŠจๅŒ–/CI -- **ๆ•ฐๆฎๅฏผๅ‡บ/ๅฏผๅ…ฅ** - ๅฏผๅ‡บๅ’Œๅฏผๅ…ฅ SSH ไธปๆœบใ€ๅ‡ญๆฎๅ’Œๆ–‡ไปถ็ฎก็†ๅ™จๆ•ฐๆฎ -- **่‡ชๅŠจ SSL ่ฎพ็ฝฎ** - ๅ†…็ฝฎ SSL ่ฏไนฆ็”Ÿๆˆๅ’Œ็ฎก็†๏ผŒๆ”ฏๆŒ HTTPS ้‡ๅฎšๅ‘ -- **็Žฐไปฃ UI** - ไฝฟ็”จ Reactใ€Tailwind CSS ๅ’Œ Shadcn ๆž„ๅปบ็š„ๆ•ดๆด็š„ๆกŒ้ข/็งปๅŠจๅ‹ๅฅฝ็•Œ้ขใ€‚ๆœ‰ๅคš็ง UI ไธป้ข˜ๅฏ้€‰๏ผŒๅŒ…ๆ‹ฌๆต…่‰ฒใ€ๆทฑ่‰ฒใ€Dracula ็ญ‰ใ€‚ไฝฟ็”จ URL ่ทฏ็”ฑๅ…จๅฑๆ‰“ๅผ€ไปปไฝ•่ฟžๆŽฅใ€‚ -- **ๅ‘ฝไปคๅކๅฒ** - ่‡ชๅŠจๅฎŒๆˆๅนถๆŸฅ็œ‹ไน‹ๅ‰่ฟ่กŒ่ฟ‡็š„ SSH ๅ‘ฝไปค -- **ๅฟซ้€Ÿ่ฟžๆŽฅ** - ๆ— ้œ€ไฟๅญ˜่ฟžๆŽฅๆ•ฐๆฎๅณๅฏ่ฟžๆŽฅๅˆฐๆœๅŠกๅ™จ -- **ๅ‘ฝไปค้ขๆฟ** - ๅŒๅ‡ปๅทฆ Shift ้”ฎๅณๅฏ้€š่ฟ‡้”ฎ็›˜ๅฟซ้€Ÿ่ฎฟ้—ฎ SSH ่ฟžๆŽฅ -- **Proxmox ้›†ๆˆ** - ไปŽๆ‚จ็š„ Proxmox ๅฎžไพ‹่‡ชๅŠจๅฐ†ไธปๆœบๆทปๅŠ ๅˆฐ Termix -- **ไธฐๅฏŒ็š„ SSH ๅŠŸ่ƒฝ** - ๆ”ฏๆŒ่ทณ่ฝฌไธปๆœบใ€Warpgateใ€ๅŸบไบŽ TOTP ็š„่ฟžๆŽฅใ€SOCKS5ใ€ไธปๆœบๅฏ†้’ฅ้ชŒ่ฏใ€ๅฏ†็ ่‡ชๅŠจๅกซๅ……ใ€[OPKSSH](https://github.com/openpubkey/opkssh)ใ€tmuxใ€็ซฏๅฃๆ•ฒๅ‡ปใ€็ปˆ็ซฏๆ—ฅๅฟ—่ฎฐๅฝ•ใ€SSH ไปฃ็†่ฝฌๅ‘ใ€Bitwarden SSH ไปฃ็†ใ€HashiCorp Vault SSH ็ญพๅ็ญ‰ -- **Termix ID** - ๅ†…็ฝฎไบŽ Termix ไธญ็š„ sshid.io ็ญ‰ๆ•ˆๅŠŸ่ƒฝใ€‚่ฎค้ข†ไธ€ไธช็”จๆˆทๅ๏ผŒๅœจ่งฃๆž URL ไธŠๅ‘ๅธƒๆ‚จ็š„ๅ…ฌๅผ€ SSH ๅฏ†้’ฅ๏ผŒๅนถไฝฟ็”จๅ†…็ฝฎ CA ็ญพๅ‘ SSH ่ฏไนฆใ€‚ +- **ไปช่กจ็›˜** - ไธ€็œผ็œ‹ๆธ…ไฝ ็š„ๆœๅŠกๅ™จ๏ผŒๅก็‰‡็”ฑไฝ ่‡ชๅทฑๆŽ’ๅธƒ +- **็ฝ‘็ปœๆ‹“ๆ‰‘ๅ›พ** - ๆ นๆฎไฝ ็š„ไธปๆœบ็ป˜ๅˆถๅ‡บไฝ ็š„ๅฎถๅบญๅฎž้ชŒๅฎค๏ผŒๅนถๆ˜พ็คบๅฎžๆ—ถ็Šถๆ€ +- **Tmux ็›‘่ง†ๅ™จ** - ๆต่งˆ tmux ็š„ไผš่ฏใ€็ช—ๅฃๅ’Œ้ขๆฟ๏ผŒๆ”ฏๆŒ้ข„่งˆๅ’Œๆœ็ดข +- **API ๅฏ†้’ฅ** - ้ขๅ‘็”จๆˆท็š„ๅฏ†้’ฅ๏ผŒๅธฆๆœ‰ๆ•ˆๆœŸ๏ผŒๅฏ็”จไบŽ่„šๆœฌๅ’Œ CI +- **ๅฏผๅ‡บไธŽๅฏผๅ…ฅ** - ๆŠŠไธปๆœบใ€ๅ‡ญๆฎๅ’Œๆ–‡ไปถ็ฎก็†ๅ™จๆ•ฐๆฎๅฏผๅ…ฅๅฏผๅ‡บ +- **่‡ชๅŠจ SSL** - ่‡ชๅŠจ็ญพๅ‘ๅ’Œ็ปญๆœŸ่ฏไนฆ๏ผŒๅนถ้…็ฝฎ HTTPS ่ทณ่ฝฌ๏ผŒไนŸๅฏไปฅไฝฟ็”จไฝ ่‡ชๅทฑ็š„่ฏไนฆ +- **ๆ•ฐๆฎๅบ“** - ้ป˜่ฎคไฝฟ็”จ SQLite๏ผŒๅŒๆ—ถๆ”ฏๆŒ PostgreSQL ๅ’Œ MySQL +- **็Žฐไปฃ็•Œ้ข** - ็ฎ€ๆด็š„ React ็•Œ้ข๏ผŒๆกŒ้ขๅ’Œ็งปๅŠจ็ซฏ้ƒฝ้€‚็”จ๏ผŒๆไพ›ๆต…่‰ฒใ€ๆทฑ่‰ฒๅ’Œ Dracula ็ญ‰ไธป้ข˜ใ€‚ไปปไฝ•่ฟžๆŽฅ้ƒฝ่ƒฝ้€š่ฟ‡็ฝ‘ๅ€ๅ…จๅฑๆ‰“ๅผ€ +- **ๅ‘ฝไปค้ขๆฟ** - ๅŒๅ‡ปๅทฆ Shift๏ผŒ็”จ้”ฎ็›˜็›ดๆŽฅ่ทณๅˆฐๆŸๅฐไธปๆœบ +- **้”ฎ็›˜ๅฟซๆท้”ฎ** - ๅœจๆ ‡็ญพ้กตไน‹้—ดๅˆ‡ๆขใ€ๅ…ณ้—ญๆ ‡็ญพ้กต็ญ‰๏ผŒๅ…จ้ƒจๅฏไปฅ้‡ๆ–ฐ็ป‘ๅฎš +- **็ฝ‘็ปœๅ”ค้†’** - ไปŽ Termix ๆˆ–่‡ชๅŠจๅŒ–ๆญฅ้ชคไธญๅ”ค้†’ไธ€ๅฐๆœบๅ™จ +- **ๅ—ไฟกไปปไปฃ็†่ฎค่ฏ** - ็”ฑๅๅ‘ไปฃ็†ๅฎŒๆˆ็™ปๅฝ•๏ผŒๅนถๆŠŠ็”จๆˆทไฟกๆฏไผ ้€’่ฟ›ๆฅ +- **ไธฐๅฏŒ็š„ SSH ๅŠŸ่ƒฝ** - ่ทณๆฟๆœบใ€Warpgateใ€TOTP ้ชŒ่ฏใ€SOCKS5ใ€ไธปๆœบๅฏ†้’ฅ้ชŒ่ฏใ€ๅฏ†็ ่‡ชๅŠจๅกซๅ……ใ€[OPKSSH](https://github.com/openpubkey/opkssh)ใ€tmuxใ€็ซฏๅฃๆ•ฒ้—จใ€็ปˆ็ซฏๆ—ฅๅฟ—ใ€ไปฃ็†่ฝฌๅ‘ใ€Bitwarden SSH ไปฃ็†ใ€HashiCorp Vault SSH ็ญพๅ็ญ‰็ญ‰ +- **Termix ID** - ๅ†…็ฝฎ็š„ sshid.io ๅผๅŠŸ่ƒฝใ€‚่ฎค้ข†ไธ€ไธช็”จๆˆทๅ๏ผŒๅœจ่งฃๆžๅœฐๅ€ไธŠๅ‘ๅธƒไฝ ็š„ๅ…ฌ้’ฅ๏ผŒๅนถ็”จๅ†…็ฝฎ CA ็ญพๅ‘ SSH ่ฏไนฆ
@@ -220,11 +279,11 @@ Termix ๆ˜ฏไธ€ไธชๅผ€ๆบใ€ๆฐธไน…ๅ…่ดนใ€่‡ชๆ‰˜็ฎก็š„ไธ€ไฝ“ๅŒ–ๆœๅŠกๅ™จ็ฎก็†ๅนณ - + - + @@ -252,9 +311,9 @@ Termix ๆ˜ฏไธ€ไธชๅผ€ๆบใ€ๆฐธไน…ๅ…่ดนใ€่‡ชๆ‰˜็ฎก็š„ไธ€ไฝ“ๅŒ–ๆœๅŠกๅ™จ็ฎก็†ๅนณ ## ๅฎ‰่ฃ… -่ฎฟ้—ฎ [Termix ๆ–‡ๆกฃ](https://docs.termix.site/install) ไบ†่งฃๆœ‰ๅ…ณๅฆ‚ไฝ•ๅœจๆ‰€ๆœ‰ๅนณๅฐไธŠๅฎ‰่ฃ… Termix ็š„ๅฎŒๆ•ด่ฏดๆ˜Žใ€‚ +่ฎฟ้—ฎ [Termix ๆ–‡ๆกฃ](https://docs.termix.site/install) ๆŸฅ็œ‹ๆ‰€ๆœ‰ๅนณๅฐ็š„ๅฎŒๆ•ดๅฎ‰่ฃ…่ฏดๆ˜Žใ€‚ -็คบไพ‹ Docker Compose ๆ–‡ไปถ๏ผˆๅฆ‚ๆžœๆ‚จไธๆ‰“็ฎ—ไฝฟ็”จ่ฟœ็จ‹ๆกŒ้ขๅŠŸ่ƒฝ๏ผŒๅฏไปฅ็œ็•ฅ `guacd` ๅ’Œ็ฝ‘็ปœ้ƒจๅˆ†๏ผ‰๏ผš +Docker Compose ็คบไพ‹๏ผˆๅฆ‚ๆžœไฝ ไธๆ‰“็ฎ—ไฝฟ็”จ่ฟœ็จ‹ๆกŒ้ขๅŠŸ่ƒฝ๏ผŒๅฏไปฅ็œ็•ฅ `guacd` ๅ’Œ็›ธๅ…ณ็ฝ‘็ปœ้…็ฝฎ๏ผ‰๏ผš ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### ๅ‘ฝไปค่กŒๅทฅๅ…ท + +Termix ่ฟ˜ๆไพ›ๅ‘ฝไปค่กŒๅทฅๅ…ท๏ผŒไฝ ๅฏไปฅๅœจ็ปˆ็ซฏ้‡Œ็ฎก็†ๆœๅŠกๅ™จ๏ผŒไนŸๅฏไปฅๆŠŠ Termix ็”จๅœจ่‡ชๅทฑ็š„่„šๆœฌไธญใ€‚ + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +ๅฎƒๅฏไปฅๆ‰“ๅผ€็ปˆ็ซฏใ€ๅœจๅ•ๅฐไธปๆœบๆˆ–ๆ•ดไธชๆœบ็พคไธŠๆ‰ง่กŒๅ‘ฝไปคใ€้€š่ฟ‡ SFTP ไผ ่พ“ๆ–‡ไปถ๏ผŒไปฅๅŠ็ฎก็†ไธปๆœบใ€ไปฃ็ ็‰‡ๆฎตๅ’Œๅ‡ญๆฎใ€‚ๅฎŒๆ•ดๆ–‡ๆกฃ่ง [docs.termix.site/cli](https://docs.termix.site/cli)ใ€‚ + +### ไบ‘็ซฏ้ƒจ็ฝฒ + +ไฝ ไนŸๅฏไปฅๆŠŠ Termix ๆœๅŠก็ซฏ่ท‘ๅœจ VPS ไธŠ๏ผŒ่€Œไธๆ˜ฏ่‡ชๅทฑ็š„ๅ†…็ฝ‘้‡Œใ€‚ๅฆ‚ๆžœ Termix ๅฐฑ่ฟ่กŒๅœจๅฎƒๆ‰€็ฎก็†็š„็ฝ‘็ปœไธญ๏ผŒไธ€ๆ—ฆ็ฝ‘็ปœๅ‡บ้—ฎ้ข˜๏ผŒTermix ไนŸไผš่ทŸ็€ไธ€่ตทๆŒ‚ๆމ๏ผŒ่€Œ่ฟ™ๆฐๆฐๆ˜ฏไฝ ๆœ€้œ€่ฆๅฎƒ็š„ๆ—ถๅ€™ใ€‚ๆ”พๅœจๅค–้ข่ฟ่กŒๅฏไปฅไฟ่ฏๅฎƒๅง‹็ปˆๅฏ่พพ๏ผŒ่ฟ˜่ƒฝ่Žทๅพ—ๅ›บๅฎš IP๏ผŒไธ็”จ VPN ๆˆ–็ซฏๅฃ่ฝฌๅ‘ๅฐฑ่ƒฝไปŽไปปไฝ•ๅœฐๆ–นๆŽฅๅ…ฅใ€‚ + +[GINERNET](https://docs.termix.site/install/ginernet) ๆ˜ฏ Termix ็š„่ตžๅŠฉๅ•†๏ผŒๆ–‡ๆกฃ้‡Œๆœ‰้ƒจ็ฝฒๅˆฐไป–ไปฌ VPS ๅนณๅฐ็š„ๅˆ†ๆญฅๆŒ‡ๅ—ใ€‚ + +
+ +## ้ฅๆต‹ + +Termix ๆฏๅคฉไผšๅ‘้€ไธ€ๆฌกๅŒฟๅ็š„ๅฐๅž‹็ปŸ่ฎกไฟกๆฏ๏ผŒ่ฎฉๆˆ‘ไบ†่งฃๆœ‰ๅคšๅฐ‘ๅฎžไพ‹ๅœจ่ฟ่กŒใ€ๅ“ชไบ›ๅŠŸ่ƒฝ่ขซ็”จๅˆฐใ€‚ๅ†…ๅฎนๅŒ…ๆ‹ฌไธ€ไธช้šๆœบ็š„ๅฎžไพ‹ IDใ€ไฝ ๆœ‰ๅคšๅฐ‘็”จๆˆทๅ’Œไธปๆœบใ€ๅบ”็”จ็‰ˆๆœฌ๏ผŒไปฅๅŠ่ฟ‡ๅŽป 24 ๅฐๆ—ถๅ†…ไฝฟ็”จไบ†ๅ“ชไบ›ๅŠŸ่ƒฝ๏ผˆ็ปˆ็ซฏใ€ๆ–‡ไปถ็ฎก็†ๅ™จใ€้šง้“ใ€Docker ็ญ‰๏ผ‰ใ€‚ๅฎƒ็ปไธๅŒ…ๅซ็”จๆˆทๅใ€ไธปๆœบๅใ€IP ๅœฐๅ€ใ€ๅ‡ญๆฎ๏ผŒๆˆ–ไปปไฝ•่ƒฝ่ฏ†ๅˆซไฝ ๅ’Œไฝ ๆœๅŠกๅ™จ็š„ไฟกๆฏใ€‚ + +่ฏฅๅŠŸ่ƒฝ้ป˜่ฎคๅผ€ๅฏใ€‚ไฝ ๅฏไปฅๅœจ็ฎก็†่ฎพ็ฝฎ็š„โ€œ้€š็”จโ€ไธญๅ…ณ้—ญๅฎƒ๏ผŒๆˆ–่€…ๅœจๅฏๅŠจ Termix ไน‹ๅ‰่ฎพ็ฝฎ `ENABLE_TELEMETRY=false`ใ€‚ +
## ๆ่ต  -Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ๆˆ–ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ๏ผŒ่ฏท่€ƒ่™‘ๆ่ต ไปฅๅธฎๅŠฉๆ”ฏไป˜ๆœๅŠกๅ™จ่ดน็”จใ€ๅŸŸๅๅ’Œๅผ€ๅ‘ๆ—ถ้—ดใ€‚ๆ่ต ่ฟ˜ๆœ‰ๅŠฉไบŽ่ต„ๅŠฉ็ ”็ฉถๅ’Œๅญฆไน ๆž„ๅปบ SAMLใ€Kubernetes ๅ’Œ Agent ๆ”ฏๆŒ็ญ‰ๅŠŸ่ƒฝๆ‰€้œ€็š„ๆ—ถ้—ดใ€‚ๅœจไธ‹ๆ–น่ฟฝ่ธช่ฟ›ๅบฆๅนถ่ฟ›่กŒๆ่ต ใ€‚ +Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ไนŸๆฒกๆœ‰ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœไฝ ่ง‰ๅพ—ๅฎƒๆœ‰็”จ๏ผŒๅฏไปฅ่€ƒ่™‘ๆ่ต ๏ผŒๅธฎๅฟ™ๅˆ†ๆ‹…ๆœๅŠกๅ™จใ€ๅŸŸๅๅ’Œๅผ€ๅ‘ๆ—ถ้—ด็š„ๆˆๆœฌใ€‚ๆ่ต ่ฟ˜่ƒฝๆ”ฏๆŒ็ ”็ฉถๅ’Œๅญฆไน  SAMLใ€Kubernetesใ€Agent ็ญ‰ๅŠŸ่ƒฝๆ‰€้œ€็š„ๆ—ถ้—ดใ€‚ๅฏไปฅๅœจไธ‹ๆ–นๆŸฅ็œ‹่ฟ›ๅฑ•ๅนถๆ่ต ใ€‚ [ๆ่ต ](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ๆˆ–ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ ## ่ตžๅŠฉๅ•† -ๆœ‰ๆ„้€š่ฟ‡ไป˜่ดนๅฑ•็คบไฝ็ฝฎๆ”ฏๆŒๅผ€ๅ‘ๅ—๏ผŸ่ฏทๅ‘้€้‚ฎไปถ่‡ณ [mail@termix.site](mailto:mail@termix.site)ใ€‚ +ๆœ‰ๆ„้€š่ฟ‡ไป˜่ดนๅฑ•็คบไฝๆ”ฏๆŒๅผ€ๅ‘ๅ—๏ผŸ่ฏทๅ‘้‚ฎไปถๅˆฐ [mail@termix.site](mailto:mail@termix.site)ใ€‚
@@ -325,10 +410,6 @@ Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ๆˆ–ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ๆˆ–ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ Rack Genius - +    + + Ginernet +

## ๆ”ฏๆŒ -ๅฆ‚ๆžœๆ‚จ้œ€่ฆ Termix ็š„ๅธฎๅŠฉๆˆ–ๆƒณ่ฆ่ฏทๆฑ‚ๅŠŸ่ƒฝ๏ผŒ่ฏท่ฎฟ้—ฎ [Issues](https://github.com/Termix-SSH/Support/issues) ้กต้ข๏ผŒ็™ปๅฝ•ๅนถ็‚นๅ‡ป `New Issue`ใ€‚่ฏทๅฐฝๅฏ่ƒฝ่ฏฆ็ป†ๅœฐๆ่ฟฐๆ‚จ็š„้—ฎ้ข˜๏ผŒๅปบ่ฎฎไฝฟ็”จ่‹ฑ่ฏญใ€‚ๆ‚จไนŸๅฏไปฅๅŠ ๅ…ฅ [Discord](https://discord.gg/jVQGdvHDrf) ๆœๅŠกๅ™จๅนถ่ฎฟ้—ฎๆ”ฏๆŒ้ข‘้“๏ผŒไฝ†ๅ“ๅบ”ๆ—ถ้—ดๅฏ่ƒฝ่พƒ้•ฟใ€‚ +้œ€่ฆๅธฎๅŠฉๆˆ–ๆƒณๆๅŠŸ่ƒฝๅปบ่ฎฎ๏ผŸๅฏไปฅ[ๆ–ฐๅปบไธ€ไธช issue](https://github.com/Termix-SSH/Support/issues)๏ผŒๅฐฝ้‡ๅ†™ๆธ…ๆฅš็ป†่Š‚๏ผŒๅฆ‚ๆžœๆ–นไพฟ่ฏท็”จ่‹ฑๆ–‡ใ€‚ไฝ ไนŸๅฏไปฅๅœจ [Discord](https://discord.gg/jVQGdvHDrf) ็š„ๆ”ฏๆŒ้ข‘้“ๆ้—ฎ๏ผŒไธ่ฟ‡้‚ฃ่พนๅ›žๅคๅฏ่ƒฝไผšๆ…ขไธ€ไบ›ใ€‚
@@ -359,7 +443,7 @@ Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ๆˆ–ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -ๅœจ YouTube ไธŠ่ง‚็œ‹ๆ›ดๆ–ฐๆฆ‚่งˆ +ๅœจ YouTube ไธŠ่ง‚็œ‹็‰ˆๆœฌๆ›ดๆ–ฐไป‹็ป

@@ -399,7 +483,7 @@ Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ๆˆ–ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ
ๅนณๅฐๅ‘่กŒ็‰ˆๅ‘่กŒๆ–นๅผ
Webไปปไฝ•็Žฐไปฃๆต่งˆๅ™จ๏ผˆChromeใ€Safariใ€Firefox๏ผ‰ยท PWA ๆ”ฏๆŒไปปไฝ•็Žฐไปฃๆต่งˆๅ™จ๏ผˆChromeใ€Safariใ€Firefox๏ผ‰ยท ๆ”ฏๆŒ PWA
Windows x64/ia32
-ๆŸไบ›่ง†้ข‘ๅ’Œๅ›พๅƒๅฏ่ƒฝๅทฒ่ฟ‡ๆ—ถ๏ผŒๆˆ–่€…ๅฏ่ƒฝๆ— ๆณ•ๅฎŒ็พŽๅฑ•็คบๅŠŸ่ƒฝใ€‚ +้ƒจๅˆ†่ง†้ข‘ๅ’Œๅ›พ็‰‡ๅฏ่ƒฝๅทฒ็ป่ฟ‡ๆ—ถ๏ผŒๆˆ–่€…ไธ่ƒฝๅฎŒๆ•ดๅฑ•็คบๅŠŸ่ƒฝใ€‚ @@ -407,10 +491,10 @@ Termix ๅ…่ดนไธ”ๅผ€ๆบ๏ผŒๆฒกๆœ‰่ฎข้˜…ๆˆ–ไป˜่ดนๆ–นๆกˆใ€‚ๅฆ‚ๆžœๆ‚จ่ง‰ๅพ—ๅฎƒๆœ‰็”จ ## ่ฎกๅˆ’ๅŠŸ่ƒฝ -ๆŸฅ็œ‹ [Projects](https://github.com/orgs/Termix-SSH/projects/5) ไบ†่งฃๆ‰€ๆœ‰่ฎกๅˆ’ๅŠŸ่ƒฝใ€‚ๅฆ‚ๆžœๆ‚จๆƒณ่ดก็Œฎไปฃ็ ๏ผŒ่ฏทๅ‚้˜… [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)ใ€‚ +ๆ‰€ๆœ‰่ฎกๅˆ’ไธญ็š„ๅŠŸ่ƒฝ้ƒฝๅœจ [Projects](https://github.com/orgs/Termix-SSH/projects/5) ้‡Œใ€‚ๅฆ‚ๆžœไฝ ๆƒณๅ‚ไธŽ่ดก็Œฎ๏ผŒ่ฏทๆŸฅ็œ‹[่ดก็ŒฎๆŒ‡ๅ—](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)ใ€‚
## ่ฎธๅฏ่ฏ -ๆ นๆฎ Apache License Version 2.0 ๅ‘ๅธƒใ€‚ๆ›ดๅคšไฟกๆฏ่ฏทๅ‚่ง `LICENSE`ใ€‚ +ๅŸบไบŽ Apache License 2.0 ๅ‘ๅธƒใ€‚่ฏฆ่ง `LICENSE`ใ€‚ diff --git a/docs/readme/README-DE.md b/docs/readme/README-DE.md index 6ca2f9a..9ac5ff6 100644 --- a/docs/readme/README-DE.md +++ b/docs/readme/README-DE.md @@ -4,7 +4,7 @@

Termix

-

Selbst gehostete SSH-Verwaltung und Remote-Desktop-Zugriff

+

Selbst gehostete Serververwaltung, von SSH und Remotedesktop bis zu Automatisierungen

English ยท @@ -37,7 +37,7 @@
-Termix ist kostenlos und Open Source. Wenn Sie es nรผtzlich finden, erwรคgen Sie eine [Spende](https://donate.termix.site/), um Serverkosten und Entwicklungszeit zu decken. +Termix ist kostenlos und quelloffen. Wenn es dir hilft, denk รผber eine [Spende](https://donate.termix.site/) nach, um Serverkosten und Entwicklungszeit zu decken.
@@ -56,9 +56,9 @@ Termix ist kostenlos und Open Source. Wenn Sie es nรผtzlich finden, erwรคgen Sie
-## Uberblick +## รœberblick -Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-Serververwaltungsplattform. Sie bietet eine plattformubergreifende Losung zur Verwaltung Ihrer Server und Infrastruktur uber eine einzige, intuitive Oberflache. Termix bietet SSH-Terminalzugriff, Remote-Desktop-Steuerung (RDP, VNC, Telnet), SSH-Tunneling-Funktionen, Remote-Dateiverwaltung und viele weitere Werkzeuge. Termix ist die perfekte kostenlose und selbst gehostete Alternative zu Termius, verfugbar fur alle Plattformen. +Termix ist eine kostenlose, quelloffene und selbst gehostete Plattform zur Verwaltung deiner Server. Sie bringt SSH-Terminals, Remotedesktops (RDP, VNC, Telnet), Dateiรผbertragungen, Tunnel, Docker, Metriken und Automatisierungen an einem Ort zusammen, im Browser, auf dem Desktop und auf dem Handy. Eine selbst gehostete Alternative zu Termius, die dauerhaft kostenlos bleibt.
@@ -68,42 +68,42 @@ Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-S -**SSH-Terminalzugriff:** -Voll ausgestattetes Terminal mit Split-Screen-Unterstutzung (bis zu 4 Panels) mit einem browserahnlichen Tab-System. Enthalt Unterstutzung fur die Anpassung des Terminals einschliesslich gangiger Terminal-Themes, Schriftarten und anderer Komponenten. +**SSH-Terminal:** +Ein vollwertiges Terminal mit Tabs wie im Browser und geteiltem Bildschirm, bis zu 6 Bereiche gleichzeitig. Thema, Schrift und Farben wรคhlst du selbst. รœber jeder Sitzung sitzt eine Leiste mit CPU, Speicher und Festplatte in Echtzeit sowie Verknรผpfungen zu Dateien, Docker, Tunneln und Metriken dieses Hosts. -**Remote-Desktop-Zugriff:** -RDP-, VNC- und Telnet-Unterstutzung uber den Browser mit vollstandiger Anpassung und Split-Screen. +**Remotedesktop:** +RDP, VNC und Telnet im Browser, in Tabs und geteiltem Bildschirm wie jede andere Sitzung. Mit Dateibrowser fรผr RDP-Laufwerke und Hochladen per Drag-and-drop. Auf dem Windows-Desktop kannst du einen Host auch im nativen RDP-Client รถffnen. -**SSH-Tunnelverwaltung:** -Erstellen und verwalten Sie Server-zu-Server-SSH-Tunnel mit automatischer Wiederverbindung, Gesundheitsuberwachung sowie lokaler, entfernter oder dynamischer SOCKS-Weiterleitung. Desktop-Client-zu-Server-Tunneleinstellungen werden lokal pro Desktop-Installation gespeichert, optionale C2S-Preset-Snapshots konnen auf dem Server gespeichert, umbenannt, geladen oder geloscht werden, wenn Sie eine lokale Tunnelkonfiguration zwischen Clients ubertragen mochten. +**SSH-Tunnel:** +Lokale, entfernte und dynamische SOCKS-Weiterleitung mit automatischem Neuverbinden und Statusprรผfungen. Client-zu-Server-Tunnel der Desktop-App bleiben auf diesem Rechner, und du kannst Voreinstellungen auf dem Server speichern, um eine Konfiguration auf einen anderen Rechner zu รผbernehmen. -**Remote-Dateimanager:** -Verwalten Sie Dateien direkt auf Remote-Servern mit Unterstutzung fur das Anzeigen und Bearbeiten von Code, Bildern, Audio und Video. Laden Sie Dateien hoch, herunter, benennen Sie sie um, loschen oder verschieben Sie sie nahtlos mit Sudo-Unterstutzung. Enthalt Unterstutzung fur das Verschieben von Dateien von Server zu Server. +**Dateimanager:** +Dateien รผber SFTP durchsuchen, bearbeiten, hochladen, herunterladen, umbenennen, verschieben und lรถschen, auch mit sudo. Code, Bilder, Audio und Video ansehen und bearbeiten. Dateien direkt von einem Server zum anderen kopieren, wobei der schnellste Weg fรผr dich gewรคhlt und die รœbertragung auf Fehler geprรผft wird. -**Docker- und Podman-Verwaltung:** -Container starten, stoppen, pausieren, entfernen. Container-Statistiken anzeigen. Container uber Docker-Exec-Terminal steuern. Unterstutzt sowohl Docker als auch Podman als Container-Laufzeitumgebung. Es wurde nicht entwickelt, um Portainer oder Dockge zu ersetzen, sondern um Ihre Container einfach zu verwalten, anstatt sie zu erstellen. +**Docker und Podman:** +Container starten, stoppen, pausieren und entfernen, ihre Auslastung ansehen und eine Shell darin รถffnen. Funktioniert mit Docker und mit Podman. Es soll Portainer oder Dockge nicht ersetzen, sondern nur die Container verwalten, die du schon hast. -**SSH-Host-Manager:** -Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern (Ordneranpassung und verschachtelte Ordner werden unterstutzt) und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Moglichkeit, die Bereitstellung von SSH-Schlusseln zu automatisieren. +**Hostverwaltung:** +Hosts mit Tags und verschachtelten Ordnern ordnen, die du benennen und einfรคrben kannst. Gespeicherte Zugangsdaten fรผr mehrere Hosts wiederverwenden, SSH-Schlรผssel automatisch verteilen, Hosts unter einem รผbergeordneten Host gruppieren, in groรŸen Mengen bearbeiten und exportieren. Fรผr einmalige Verbindungen, die du nicht speichern willst, gibt es Schnellverbindung. @@ -111,83 +111,139 @@ Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ord **Host-Metriken:** -CPU-, Arbeitsspeicher- und Festplattenauslastung, Netzwerk, Betriebszeit, Systeminformationen, Firewall, Port-Monitor, Log-Viewer, Benutzer/Berechtigungen, Zertifikate und vieles mehr anzeigen, was auf den meisten Linux-basierten Servern funktioniert. Enthalt Zeitreihen-Verlaufsdiagramme und schwellenwertbasierte Warnmeldungen mit ntfy- und Webhook-Unterstutzung. +CPU, Speicher, Festplatte, Netzwerk, Temperatur, Laufzeit, Prozesse, Ports, Anmeldungen und Systeminfos auf den meisten Linux-Servern, mit Verlaufsgrafiken. รœber Verwaltungskarten kรผmmerst du dich um Dienste, Cronjobs, Pakete, Benutzer, Firewallregeln, WireGuard, Tailscale, SSL-Zertifikate, Logs und Statusprรผfungen, ohne Termix zu verlassen. -**Benutzerauthentifizierung:** -Sichere Benutzerverwaltung mit Admin-Kontrollen (kann Informationen anderer Benutzer bearbeiten) und OIDC-/LDAP-/SSO-Unterstutzung (mit Zugriffskontrolle), 2FA (TOTP) und Passkey (WebAuthn)-Unterstutzung. Aktive Benutzersitzungen uber alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknupfen. Audit-Protokoll aller Benutzeraktionen anzeigen. +**Automatisierungen:** +Wรคhle einen Auslรถser und lege fest, was passieren soll. Auslรถser sind unter anderem eine Metrik รผber einem Schwellwert, ein Host der hoch- oder runtergeht, eine geรคnderte Statusprรผfung, ein Zeitplan, ein Container-Ereignis oder ein eingehender Webhook. Schritte kรถnnen Befehle und Snippets ausfรผhren, Container und Tunnel steuern, einen Host aufwecken, eine URL aufrufen, warten, sich nach einer Bedingung verzweigen, eine andere Automatisierung starten und dich รผber ntfy, Discord oder einen Webhook benachrichtigen. Mit Testlรคufen probierst du alles gefahrlos aus. -**Tailscale-Integration:** -Gerate aus Ihrem Tailnet auflisten, um sie schnell als Hosts hinzuzufugen, und mit Tailscale SSH als Authentifizierungsmethode verbinden, sodass Ihre Tailnet-ACLs die Autorisierung ubernehmen, ohne Anmeldedaten speichern zu mussen. +**Flotten:** +Fasse Hosts zu einer Flotte zusammen, entweder von Hand oder รผber Tag-Regeln, damit neue Hosts von selbst dazukommen. Fรผhre einen Befehl auf allen Hosts gleichzeitig aus, schiebe und hole Dateien auf allen, installiere Pakete und sammle eine รœbersicht รผber Betriebssystem, Kernel, Architektur und Laufzeit. -**RBAC/Freigabe:** -Erstellen Sie Rollen und teilen Sie Hosts uber Benutzer/Rollen hinweg. Unterstutzt alle Authentifizierungstypen und alle Host-Protokolle. +**KI-Assistent:** +Optional und aus, bis du ihn einschaltest. Verbinde OpenAI, Anthropic, Gemini, Ollama oder einen beliebigen OpenAI-kompatiblen Endpunkt und frag ihn zu deiner Umgebung. Er liest Hosts, Flotten, Snippets und Warnungen und schlรคgt ร„nderungen vor, die du bestรคtigst, statt sie selbst vorzunehmen. An Zugangsdaten, Benutzer und Einstellungen kommt er nie heran. Administratoren kรถnnen ihn fรผr die ganze Instanz auslassen, und du kannst ihn schon bei der Einrichtung ausblenden. -**Serielle Verbindungen:** -Verbinden Sie sich direkt vom Browser oder der Desktop-App aus mit seriellen Geraten (Router, Switches, Mikrocontroller usw.). Konfigurieren Sie Baudrate, Datenbits, Stoppbits und Paritat. Verwendet die Web Serial API in unterstutzten Browsern oder ein natives Backend in der Electron-App. +**Anmeldung und Benutzer:** +Lokale Konten sowie Anmeldung รผber OIDC, LDAP, GitHub und Google, dazu Zwei-Faktor-Authentifizierung (TOTP), Passkeys (WebAuthn) und vertrauenswรผrdige Gerรคte. Administratoren kรถnnen Benutzer verwalten, OIDC-Gruppen auf Rollen abbilden, alle aktiven Sitzungen รผber alle Plattformen hinweg sehen und beenden. Verknรผpfe dein lokales Konto mit deinem OIDC-Konto und lies im Prรผfprotokoll nach, wer was gemacht hat. -**Warnmeldungen:** -Legen Sie schwellenwertbasierte Warnregeln fur Host-Metriken (CPU, Arbeitsspeicher, Festplatte usw.) fest und erhalten Sie Benachrichtigungen uber ntfy oder Webhooks, wenn diese ausgelost werden. Zeigen Sie ausgeloste und aufgeloste Warnmeldungen in einem Verlaufsprotokoll an. +**Rollen und Freigaben:** +Lege Rollen an und teile Hosts mit Benutzern oder Rollen auf vier Stufen: Verbinden, Ansehen, Bearbeiten und Verwalten. Das funktioniert mit jeder Authentifizierungsart und jedem Protokoll, und du kannst die Zugangsdaten fรผr einen geteilten Host รผberschreiben. +**Warnungen:** +Lege Regeln fรผr Host-Metriken wie CPU, Speicher und Festplatte fest und lass dich รผber ntfy, Discord oder einen Webhook benachrichtigen, wenn sie greifen. Sieh dir aktive und wieder behobene Warnungen im Verlauf an und blende aus, was dich nicht interessiert. + + + + **Startseite:** -Eine vollstandig anpassbare Startseite mit einem Drag-and-Drop-Widget-Raster. Fugen Sie Widgets fur Hoststatus, Service-Links, Uhren, Notizen, RSS-Feeds, Wetter, Docker-Container, Host-Metrik-Diagramme, eingebettete Terminals, iFrames und mehr hinzu. - - - - -**Datenbankverschlusselung:** -Backend gespeichert als verschlusselte SQLite-Datenbankdateien. Weitere Informationen in der [Dokumentation](https://docs.termix.site/security). +Ein Raster aus Widgets, das du selbst per Drag-and-drop zusammenstellst. Widgets fรผr Hoststatus, Pings, Dienstlinks, Lesezeichen, Suche, Uhren, Kalender, Countdowns, Notizen, RSS, Wetter, Bilder, Iframes, Docker, Tunnel, Metrikdiagramme, eigene APIs und sogar ein laufendes Terminal. -**Netzwerkgraph:** -Passen Sie Ihr Dashboard an, um Ihr Homelab basierend auf Ihren SSH-Verbindungen mit Statusunterstutzung zu visualisieren. +**Snippets und Werkzeuge:** +Speichere Befehle, die du oft brauchst, und starte sie mit einem Klick, mit Variablen fรผr den Host und eigene Eingaben. Fรผhre einen Befehl in allen offenen Terminals zugleich aus und durchsuche deinen Befehlsverlauf mit Autovervollstรคndigung. -**SSH-Werkzeuge:** -Erstellen Sie wiederverwendbare Befehlsvorlagen, die mit einem einzigen Klick ausgefuhrt werden. Fuhren Sie einen Befehl gleichzeitig in mehreren geoffneten Terminals aus. +**Sitzungsfreigabe:** +Teile eine laufende Terminal-, RDP-, VNC- oder Telnet-Sitzung in Echtzeit. Verschicke einen Link, dem jeder ohne Konto beitreten kann, oder teile mit einem bestimmten Termix-Benutzer, nur lesend oder mit Schreibrechten. Freigaben kรถnnen von selbst ablaufen oder zurรผckgezogen werden und lassen sich global oder pro Host abschalten. -**Persistente Tabs:** -SSH-Sitzungen und Tabs bleiben uber Gerate/Aktualisierungen hinweg offen, wenn im Benutzerprofil aktiviert. +**Sitzungsaufzeichnung und Protokolle:** +Zeichne Terminal-, RDP- und VNC-Sitzungen auf und spiel sie spรคter ab. Lade einfache Textprotokolle einer Sitzung herunter und sieh im Verbindungsprotokoll nach, was wรคhrend einer Verbindung genau passiert ist. + + + + +**Serielle Verbindungen:** +Sprich mit seriellen Gerรคten wie Routern, Switches und Mikrocontrollern, aus dem Browser oder der Desktop-App. Stelle Baudrate, Datenbits, Stoppbits und Paritรคt ein. Nutzt die Web-Serial-API in passenden Browsern oder ein natives Backend in der Desktop-App. + + + + + + +**Tailscale:** +Hol Gerรคte aus deinem Tailnet, um sie mit ein paar Klicks als Hosts anzulegen, und verbinde dich per Tailscale SSH, damit deine Tailnet-ACLs den Zugriff regeln und keine Zugangsdaten gespeichert werden. Headscale und eigene Endpunkte gehen auch. + + + + +**Proxmox:** +Importiere Hosts direkt aus einer Proxmox-Instanz und beobachte Knoten- und Gastwerte wie CPU, Speicher und Storage in einem eigenen Tab. + + + + + + +**Arbeitsbereiche und Tabs:** +Speichere eine Reihe von Tabs samt Aufteilung und รถffne alles mit einem Klick wieder. Termix merkt sich auch deine letzte Sitzung, sodass deine Tabs nach einem Neuladen und auf anderen Gerรคten wieder da sind. + + + + +**Gefรผhrte Einrichtung:** +Eine kurze Einrichtung fรผhrt dich durch die Wahl einer Oberflรคchenvorlage, deines Themas, der gewรผnschten Funktionen und deines ersten Hosts. Der einfache Modus blendet aus, was du nicht nutzt, und du kannst die Einrichtung jederzeit erneut starten oder die Vorlage wechseln. + + + + + + +**Desktop eigenstรคndig und Synchronisierung:** +Die Desktop-App lรคuft eigenstรคndig mit lokalem Backend und eigener Datenbank, ganz ohne Server. Du kannst sie auch mit einem Termix-Server verbinden, um Hosts, Zugangsdaten, Snippets und mehr in beide Richtungen abzugleichen, und wรคhlen, ob Verbindungen lokal oder รผber den Server aufgebaut werden. + + + + +**Kommandozeile:** +Ein `termix`-CLI fรผr deine Shell und deine Skripte. Terminals รถffnen, einen Befehl auf einem Host oder einer ganzen Flotte ausfรผhren, Dateien per SFTP verschieben und Hosts, Snippets und Zugangsdaten verwalten. Installiere es mit `npm install -g @termix-cli/cli` oder nimm eine eigenstรคndige Binรคrdatei. Siehe die [CLI-Dokumentation](https://docs.termix.site/cli). + + + + + + +**Sicherheit:** +Passwรถrter, Schlรผssel und andere Geheimnisse werden pro Benutzer verschlรผsselt, und die Datenbankdateien selbst lassen sich auf der Festplatte verschlรผsseln. Wie das funktioniert, steht in der [Dokumentation](https://docs.termix.site/security). **Sprachen:** -Integrierte Unterstutzung fur ca. 30 Sprachen (verwaltet uber [Crowdin](https://docs.termix.site/translations)). +Rund 30 Sprachen sind eingebaut, verwaltet รผber [Crowdin](https://docs.termix.site/translations). @@ -199,36 +255,39 @@ Integrierte Unterstutzung fur ca. 30 Sprachen (verwaltet uber [Crowdin](https://

Weitere Funktionen
-- **Dashboard** - Serverinformationen auf einen Blick auf Ihrem Dashboard anzeigen -- **API-Schlussel** - Erstellen Sie benutzerbezogene API-Schlussel mit Ablaufdaten zur Verwendung fur Automatisierung/CI -- **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren -- **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen -- **Moderne Benutzeroberflache** - Saubere desktop-/mobilfreundliche Oberflache, erstellt mit React, Tailwind CSS und Shadcn. Wahlen Sie zwischen vielen verschiedenen UI-Themes einschliesslich Hell, Dunkel, Dracula usw. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu offnen. -- **Befehlsverlauf** - Autovervollstandigung und Anzeige zuvor ausgefuhrter SSH-Befehle -- **Schnellverbindung** - Verbinden Sie sich mit einem Server, ohne die Verbindungsdaten speichern zu mussen -- **Befehlspalette** - Doppeltippen Sie die linke Umschalttaste, um schnell auf SSH-Verbindungen mit Ihrer Tastatur zuzugreifen -- **Proxmox-Integration** - Automatisches Hinzufugen von Hosts zu Termix aus Ihrer Proxmox-Instanz -- **SSH-Funktionsreich** - Unterstutzt Jump-Hosts, Warpgate, TOTP-basierte Verbindungen, SOCKS5, Host-Key-Verifizierung, automatisches Ausfullen von Passwortern, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, Port Knocking, Terminal-Protokollierung, SSH-Agent-Forwarding, Bitwarden SSH-Agent, HashiCorp Vault SSH-Signierung und mehr. -- **Termix ID** - Ein sshid.io-Aquivalent, integriert in Termix. Beanspruchen Sie einen Handle, veroffentlichen Sie Ihre offentlichen SSH-Schlussel unter einer Resolver-URL und nutzen Sie eine integrierte CA zur Ausstellung von SSH-Zertifikaten. +- **Dashboard** - Deine Server auf einen Blick, mit Karten, die du selbst anordnest +- **Netzwerkgrafik** - Dein Homelab aus deinen Hosts gezeichnet, mit Live-Status +- **Tmux-Monitor** - tmux-Sitzungen, Fenster und Bereiche durchsehen, mit Vorschau und Suche +- **API-Schlรผssel** - Benutzerbezogene Schlรผssel mit Ablaufdatum fรผr Skripte und CI +- **Export und Import** - Hosts, Zugangsdaten und Dateimanager-Daten rein- und rausholen +- **Automatisches SSL** - Zertifikate werden fรผr dich erstellt und erneuert, samt HTTPS-Weiterleitung, oder du bringst eigene mit +- **Datenbanken** - StandardmรครŸig SQLite, dazu PostgreSQL und MySQL +- **Moderne Oberflรคche** - Aufgerรคumte React-Oberflรคche fรผr Desktop und Handy, mit Themen wie Hell, Dunkel und Dracula. Jede Verbindung lรคsst sich รผber eine URL im Vollbild รถffnen +- **Befehlspalette** - Zweimal linke Umschalttaste, um per Tastatur zu einem Host zu springen +- **Tastenkรผrzel** - Zwischen Tabs wechseln, Tabs schlieรŸen und mehr, alles neu belegbar +- **Wake-on-LAN** - Einen Rechner aus Termix heraus oder aus einem Automatisierungsschritt aufwecken +- **Vertrauenswรผrdiger Proxy** - Einen Reverse Proxy die Anmeldung erledigen und den Benutzer durchreichen lassen +- **Viele SSH-Funktionen** - Sprunghosts, Warpgate, TOTP-Abfragen, SOCKS5, Prรผfung von Hostschlรผsseln, automatisches Ausfรผllen von Passwรถrtern, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, Port Knocking, Terminalprotokolle, Agent-Weiterleitung, Bitwarden SSH-Agent, SSH-Signierung รผber HashiCorp Vault und mehr +- **Termix ID** - Eine eingebaute Variante von sshid.io. Sichere dir einen Namen, verรถffentliche deine รถffentlichen Schlรผssel unter einer Resolver-URL und stelle SSH-Zertifikate รผber die eingebaute CA aus
-## Plattformunterstutzung +## Unterstรผtzte Plattformen - + - + - + @@ -252,9 +311,9 @@ Integrierte Unterstutzung fur ca. 30 Sprachen (verwaltet uber [Crowdin](https:// ## Installation -Besuchen Sie die [Termix-Dokumentation](https://docs.termix.site/install) fur vollstandige Installationsanleitungen fur alle Plattformen. +In der [Termix-Dokumentation](https://docs.termix.site/install) findest du die vollstรคndigen Installationsanleitungen fรผr alle Plattformen. -Beispiel einer Docker-Compose-Datei (Sie konnen `guacd` und das Netzwerk weglassen, wenn Sie keine Remote-Desktop-Funktionen nutzen mochten): +Beispiel fรผr eine Docker-Compose-Datei (`guacd` und das Netzwerk kannst du weglassen, wenn du keinen Remotedesktop brauchst): ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### Kommandozeile + +Termix hat auch ein CLI, damit du deine Server vom Terminal aus verwalten und Termix in eigenen Skripten nutzen kannst. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +Es kann Terminals รถffnen, einen Befehl auf einem Host oder einer ganzen Flotte ausfรผhren, Dateien per SFTP verschieben und Hosts, Snippets und Zugangsdaten verwalten. Die vollstรคndige Dokumentation steht auf [docs.termix.site/cli](https://docs.termix.site/cli). + +### Cloud-Hosting + +Du kannst den Termix-Server auf einem VPS laufen lassen statt im eigenen Netz. Lรคuft Termix in dem Netz, das es verwaltet, reiรŸt eine Stรถrung es mit sich, und zwar genau dann, wenn du es zum Reparieren brรคuchtest. Woanders bleibt es erreichbar, du bekommst eine feste IP und kommst von รผberall heran, ohne VPN und ohne Portfreigabe. + +[GINERNET](https://docs.termix.site/install/ginernet) sponsert Termix, und in der Dokumentation steht eine Schritt-fรผr-Schritt-Anleitung fรผr die Bereitstellung auf deren VPS-Plattform. + +
+ +## Telemetrie + +Termix schickt einmal am Tag ein kleines anonymes Signal, damit ich sehen kann, wie viele Instanzen laufen und welche Funktionen genutzt werden. Enthalten sind eine zufรคllige Instanz-ID, wie viele Benutzer und Hosts du hast, die App-Version und welche Funktionen (Terminal, Dateimanager, Tunnel, Docker usw.) in den letzten 24 Stunden benutzt wurden. Niemals enthalten sind Benutzernamen, Hostnamen, IP-Adressen, Zugangsdaten oder irgendetwas anderes, das dich oder deine Server identifiziert. + +Es ist standardmรครŸig an. Schalte es in den Administrationseinstellungen unter Allgemein aus oder setze `ENABLE_TELEMETRY=false`, bevor du Termix รผberhaupt startest. +
## Spenden -Termix ist kostenlos und Open Source, ohne Abonnements oder kostenpflichtige Plane. Wenn Sie es nutzlich finden, erwagen Sie eine Spende, um Serverkosten, Domains und Entwicklungszeit zu decken. Spenden helfen auch dabei, die Zeit zu finanzieren, die benotigt wird, um zu erforschen und zu lernen, was fur Funktionen wie SAML-, Kubernetes- und Agent-Unterstutzung erforderlich ist. Verfolgen Sie den Fortschritt und spenden Sie unten. +Termix ist kostenlos und quelloffen, ohne Abo und ohne Bezahlmodell. Wenn es dir hilft, denk รผber eine Spende nach, um Server, Domains und Entwicklungszeit zu decken. Spenden finanzieren auch die Zeit, um Funktionen wie SAML, Kubernetes und Agent-Unterstรผtzung zu erarbeiten. Unten kannst du den Fortschritt verfolgen und spenden. [Spenden](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix ist kostenlos und Open Source, ohne Abonnements oder kostenpflichtige Pla ## Sponsoren -Interessiert an einer bezahlten Platzierung zur Unterstutzung der Entwicklung? Schreiben Sie eine E-Mail an [mail@termix.site](mailto:mail@termix.site). +Interesse an einer bezahlten Platzierung zur Unterstรผtzung der Entwicklung? Schreib an [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Interessiert an einer bezahlten Platzierung zur Unterstutzung der Entwicklung? S Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Interessiert an einer bezahlten Platzierung zur Unterstutzung der Entwicklung? S Rack Genius - +    + + Ginernet +

## Support -Wenn Sie Hilfe benotigen oder eine Funktion fur Termix anfragen mochten, besuchen Sie die [Issues](https://github.com/Termix-SSH/Support/issues)-Seite, melden Sie sich an und klicken Sie auf `New Issue`. Bitte beschreiben Sie Ihr Anliegen so detailliert wie moglich, vorzugsweise auf Englisch. Sie konnen auch dem [Discord](https://discord.gg/jVQGdvHDrf)-Server beitreten und den Support-Kanal besuchen, allerdings konnen die Antwortzeiten dort langer sein. +Brauchst du Hilfe oder mรถchtest du eine Funktion vorschlagen? Erstelle ein [neues Issue](https://github.com/Termix-SSH/Support/issues) und beschreibe es so genau wie mรถglich, nach Mรถglichkeit auf Englisch. Du kannst auch im Support-Kanal auf [Discord](https://discord.gg/jVQGdvHDrf) fragen, dort dauern Antworten aber manchmal lรคnger.
@@ -359,7 +443,7 @@ Wenn Sie Hilfe benotigen oder eine Funktion fur Termix anfragen mochten, besuche [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -Update-Ubersichten auf YouTube ansehen +รœbersichten zu Updates auf YouTube ansehen

@@ -399,7 +483,7 @@ Wenn Sie Hilfe benotigen oder eine Funktion fur Termix anfragen mochten, besuche
PlattformDistributionBezugsquelle
WebJeder moderne Browser (Chrome, Safari, Firefox) ยท PWA-UnterstutzungJeder moderne Browser (Chrome, Safari, Firefox) ยท PWA-fรคhig
Windows x64/ia32Portabel ยท MSI-Installationsprogramm ยท ChocolateyPortabel ยท MSI-Installer ยท Chocolatey
Linux x64/ia32
-Einige Videos und Bilder konnen veraltet sein oder Funktionen moglicherweise nicht perfekt darstellen. +Manche Videos und Bilder sind vielleicht veraltet oder zeigen die Funktionen nicht perfekt. @@ -407,10 +491,10 @@ Wenn Sie Hilfe benotigen oder eine Funktion fur Termix anfragen mochten, besuche ## Geplante Funktionen -Siehe [Projekte](https://github.com/orgs/Termix-SSH/projects/5) fur alle geplanten Funktionen. Wenn Sie beitragen mochten, siehe [Mitwirken](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +Alle geplanten Funktionen stehen unter [Projects](https://github.com/orgs/Termix-SSH/projects/5). Wenn du mitarbeiten mรถchtest, sieh dir [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) an.
## Lizenz -Verteilt unter der Apache License Version 2.0. Siehe `LICENSE` fur weitere Informationen. +Verรถffentlicht unter der Apache-Lizenz Version 2.0. Mehr dazu in `LICENSE`. diff --git a/docs/readme/README-ES.md b/docs/readme/README-ES.md index b268940..a090af6 100644 --- a/docs/readme/README-ES.md +++ b/docs/readme/README-ES.md @@ -4,7 +4,7 @@

Termix

-

Gestiรณn SSH autoalojada y acceso a escritorio remoto

+

Gestiรณn de servidores autoalojada, desde SSH y escritorio remoto hasta automatizaciones

English ยท @@ -37,7 +37,7 @@
-Termix es gratuito y de cรณdigo abierto. Si lo encuentras รบtil, considera [donar](https://donate.termix.site/) para ayudar a cubrir los costos del servidor y el tiempo de desarrollo. +Termix es gratuito y de cรณdigo abierto. Si te resulta รบtil, considera [donar](https://donate.termix.site/) para ayudar a cubrir los costes de servidor y el tiempo de desarrollo.
@@ -49,145 +49,201 @@ Termix es gratuito y de cรณdigo abierto. Si lo encuentras รบtil, considera [dona

Repo of the Day Achievement
- Logrado el 1 de septiembre de 2025 + Conseguido el 1 de septiembre de 2025


-## Descripcion General +## Descripciรณn general -Termix es una plataforma de gestion de servidores todo en uno, de codigo abierto, siempre gratuita y autoalojada. Proporciona una solucion multiplataforma para gestionar sus servidores e infraestructura a traves de una interfaz unica e intuitiva. Termix ofrece acceso a terminal SSH, control de escritorio remoto (RDP, VNC, Telnet), capacidades de tuneles SSH, gestion remota de archivos y muchas otras herramientas. Termix es la alternativa perfecta, gratuita y autoalojada a Termius, disponible para todas las plataformas. +Termix es una plataforma gratuita, de cรณdigo abierto y autoalojada para gestionar tus servidores. Reรบne en un solo sitio terminales SSH, escritorios remotos (RDP, VNC, Telnet), transferencias de archivos, tรบneles, Docker, mรฉtricas y automatizaciones, en web, escritorio y mรณvil. Es una alternativa autoalojada a Termius que seguirรก siendo gratuita.
-## Caracteristicas +## Caracterรญsticas + + + + + + + + + + + + + + + + @@ -196,35 +252,38 @@ Soporte integrado para aproximadamente 30 idiomas (gestionado por [Crowdin](http
-Mas caracteristicas +Mรกs caracterรญsticas
-- **Dashboard** - Vea la informacion del servidor de un vistazo en su dashboard -- **Claves API** - Cree claves API con ambito de usuario y fechas de vencimiento para usar en automatizacion/CI -- **Exportacion/Importacion de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos -- **Configuracion Automatica de SSL** - Generacion y gestion integrada de certificados SSL con redirecciones HTTPS -- **Interfaz Moderna** - Interfaz limpia compatible con escritorio/movil construida con React, Tailwind CSS y Shadcn. Elija entre muchos temas de UI diferentes, incluyendo claro, oscuro, Dracula, etc. Use rutas URL para abrir cualquier conexion en pantalla completa. -- **Historial de Comandos** - Autocompletado y visualizacion de comandos SSH ejecutados anteriormente -- **Conexion Rapida** - Conectese a un servidor sin necesidad de guardar los datos de conexion -- **Paleta de Comandos** - Pulse dos veces la tecla Shift izquierda para acceder rapidamente a las conexiones SSH con su teclado -- **Integracion con Proxmox** - Agregue automaticamente hosts a Termix desde su instancia de Proxmox -- **SSH Rico en Funciones** - Soporta jump hosts, Warpgate, conexiones basadas en TOTP, SOCKS5, verificacion de clave de host, autocompletado de contrasenas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, registro de terminal, reenvio de agente SSH, agente SSH de Bitwarden, firma SSH con HashiCorp Vault y mas. -- **Termix ID** - Un equivalente a sshid.io integrado en Termix. Reclame un identificador, publique sus claves publicas SSH en una URL de resolucion y use una CA integrada para emitir certificados SSH. +- **Panel** - Tus servidores de un vistazo, con tarjetas que colocas tรบ +- **Grรกfico de red** - Tu homelab dibujado a partir de tus hosts, con estado en vivo +- **Monitor de tmux** - Revisa sesiones, ventanas y paneles de tmux, con vista previa y bรบsqueda +- **Claves de API** - Claves por usuario con fecha de caducidad para scripts y CI +- **Exportar e importar** - Mueve hosts, credenciales y datos del gestor de archivos +- **SSL automรกtico** - Certificados generados y renovados por ti, con redirecciรณn a HTTPS, o usa los tuyos +- **Bases de datos** - SQLite por defecto, y tambiรฉn PostgreSQL y MySQL +- **Interfaz moderna** - Una interfaz React limpia que funciona en escritorio y mรณvil, con temas como claro, oscuro y Dracula. Cualquier conexiรณn se puede abrir a pantalla completa desde una URL +- **Paleta de comandos** - Pulsa dos veces Mayรบs izquierda para ir a un host desde el teclado +- **Atajos de teclado** - Moverte entre pestaรฑas, cerrarlas y mรกs, todo reasignable +- **Wake-on-LAN** - Enciende una mรกquina desde Termix o desde un paso de automatizaciรณn +- **Proxy de confianza** - Deja que un proxy inverso gestione el acceso y pase al usuario +- **SSH muy completo** - Hosts de salto, Warpgate, peticiones TOTP, SOCKS5, verificaciรณn de claves de host, autorrelleno de contraseรฑas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, registro del terminal, reenvรญo de agente, agente SSH de Bitwarden, firma SSH con HashiCorp Vault y mรกs +- **Termix ID** - Una versiรณn integrada de sshid.io. Reserva un identificador, publica tus claves pรบblicas en una URL de resoluciรณn y emite certificados SSH desde la CA integrada

-## Soporte de Plataformas +## Plataformas compatibles
-**Acceso a Terminal SSH:** -Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestanas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes. +**Terminal SSH:** +Un terminal completo con pestaรฑas como las del navegador y pantalla dividida, hasta 6 paneles a la vez. Elige tu tema, tu fuente y tus colores. Sobre cada sesiรณn hay una barra con CPU, memoria y disco en vivo, ademรกs de accesos rรกpidos a los archivos, Docker, tรบneles y mรฉtricas de ese host. -**Acceso a Escritorio Remoto:** -Soporte RDP, VNC y Telnet a traves del navegador con personalizacion completa y pantalla dividida. +**Escritorio remoto:** +RDP, VNC y Telnet en el navegador, en pestaรฑas y pantalla dividida como cualquier otra sesiรณn. Incluye un explorador de archivos para las unidades RDP y subida arrastrando y soltando. En el escritorio de Windows tambiรฉn puedes abrir un host en el cliente RDP nativo.
-**Gestion de Tuneles SSH:** -Cree y gestione tuneles SSH de servidor a servidor con reconexion automatica, monitoreo de estado y reenvio local, remoto o dinamico SOCKS. La configuracion de tuneles de cliente de escritorio a servidor se almacena localmente por instalacion de escritorio, los snapshots de presets C2S opcionales pueden guardarse en el servidor, renombrarse, cargarse o eliminarse cuando desee mover una configuracion de tunel local entre clientes. +**Tรบneles SSH:** +Reenvรญo local, remoto y SOCKS dinรกmico, con reconexiรณn automรกtica y comprobaciones de estado. Los tรบneles de cliente a servidor de la aplicaciรณn de escritorio se guardan en ese equipo, y puedes guardar ajustes en el servidor para llevarte una configuraciรณn a otro equipo. -**Gestor Remoto de Archivos:** -Gestione archivos directamente en servidores remotos con soporte para visualizar y editar codigo, imagenes, audio y video. Suba, descargue, renombre, elimine y mueva archivos sin problemas con soporte sudo. Incluye soporte para mover archivos de servidor a servidor. +**Gestor de archivos:** +Navega, edita, sube, descarga, renombra, mueve y borra archivos por SFTP, con soporte para sudo. Mira y edita cรณdigo, imรกgenes, audio y vรญdeo. Copia archivos directamente de un servidor a otro, con la ruta mรกs rรกpida elegida por ti y las transferencias verificadas.
-**Gestion de Docker y Podman:** -Inicie, detenga, pause, elimine contenedores. Vea estadisticas de contenedores. Controle contenedores usando el terminal docker exec. Compatible con Docker y Podman como entorno de ejecucion de contenedores. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos. +**Docker y Podman:** +Arranca, para, pausa y elimina contenedores, mira sus estadรญsticas y abre una consola dentro de uno. Funciona con Docker y con Podman. No pretende sustituir a Portainer ni a Dockge, solo gestionar los contenedores que ya tienes. -**Gestor de Hosts SSH:** -Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas (con personalizacion de carpetas y soporte de carpetas anidadas), y guarde facilmente informacion de inicio de sesion reutilizable con la capacidad de automatizar el despliegue de claves SSH. +**Gestor de hosts:** +Guarda y organiza hosts con etiquetas y carpetas anidadas que puedes nombrar y colorear. Reutiliza credenciales guardadas entre hosts, despliega claves SSH automรกticamente, agrupa hosts bajo un host padre, edita y exporta en lote, y usa la conexiรณn rรกpida para conexiones puntuales que no quieres guardar.
-**Metricas del Host:** -Vea el uso de CPU, memoria y disco, red, tiempo de actividad, informacion del sistema, firewall, monitor de puertos, visor de registros, usuarios/permisos, certificados y muchos mas, que funcionan en la mayoria de los servidores basados en Linux. Incluye graficos de historial de series temporales y alertas basadas en umbrales con soporte para ntfy y webhooks. +**Mรฉtricas de host:** +CPU, memoria, disco, red, temperatura, tiempo encendido, procesos, puertos, inicios de sesiรณn e informaciรณn del sistema en la mayorรญa de servidores Linux, con grรกficas de histรณrico. Las tarjetas de gestiรณn te dejan manejar servicios, tareas cron, paquetes, usuarios, reglas del cortafuegos, WireGuard, Tailscale, certificados SSL, registros y comprobaciones de estado sin salir de Termix. -**Autenticacion de Usuarios:** -Gestion segura de usuarios con controles de administrador (puede editar la informacion de otros usuarios) y soporte para OIDC/LDAP/SSO (con control de acceso), 2FA (TOTP) y soporte para passkeys (WebAuthn). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre si. Vea el registro de auditoria de las acciones de todos los usuarios. +**Automatizaciones:** +Elige un disparador y luego di quรฉ debe pasar. Los disparadores incluyen una mรฉtrica que supera un umbral, un host que se cae o vuelve, una comprobaciรณn de estado que cambia, una programaciรณn, un evento de contenedor o un webhook entrante. Los pasos pueden ejecutar comandos y fragmentos, controlar contenedores y tรบneles, despertar un host, llamar a una URL, esperar, ramificarse segรบn una condiciรณn, ejecutar otra automatizaciรณn y avisarte por ntfy, Discord o un webhook. Las ejecuciones de prueba te dejan probarlo sin riesgo.
-**Integracion con Tailscale:** -Liste dispositivos de su red Tailscale para agregarlos rapidamente como hosts y conectese usando Tailscale SSH como metodo de autenticacion, permitiendo que las ACL de Tailscale gestionen la autorizacion sin almacenar credenciales. +**Flotas:** +Agrupa hosts en una flota eligiรฉndolos o con reglas de etiquetas, para que los nuevos entren solos. Ejecuta un comando en todos los hosts a la vez, envรญa y recoge archivos de todos ellos, instala paquetes y reรบne un inventario del sistema, el kernel, la arquitectura y el tiempo encendido. -**RBAC/Compartir:** -Cree roles y comparta hosts entre usuarios/roles. Compatible con todos los tipos de autenticacion y todos los protocolos de host. +**Asistente de IA:** +Es opcional y estรก apagado hasta que tรบ lo enciendas. Conecta OpenAI, Anthropic, Gemini, Ollama o cualquier punto de acceso compatible con OpenAI y pregรบntale sobre tu instalaciรณn. Puede leer hosts, flotas, fragmentos y alertas, y propone cambios para que los apruebes en lugar de hacerlos รฉl. Nunca puede tocar credenciales, usuarios ni ajustes. Los administradores pueden dejarlo apagado para toda la instancia, y tรบ puedes ocultarlo durante la configuraciรณn.
-**Conexiones Serie:** -Conectese a dispositivos serie (routers, switches, microcontroladores, etc.) directamente desde el navegador o la aplicacion de escritorio. Configure la tasa de baudios, bits de datos, bits de parada y paridad. Utiliza la Web Serial API en navegadores compatibles o un backend nativo en la aplicacion Electron. +**Acceso y usuarios:** +Cuentas locales mรกs inicio de sesiรณn con OIDC, LDAP, GitHub y Google, con doble factor (TOTP), llaves de acceso (WebAuthn) y dispositivos de confianza. Los administradores pueden gestionar usuarios, asignar grupos de OIDC a roles, ver todas las sesiones activas en cualquier plataforma y revocarlas. Enlaza tu cuenta local con la de OIDC y consulta el registro de auditorรญa de lo que ha hecho cada uno. +**Roles y comparticiรณn:** +Crea roles y comparte hosts con usuarios o roles en cuatro niveles: conectar, ver, editar y gestionar. Funciona con todos los tipos de autenticaciรณn y todos los protocolos, y puedes cambiar las credenciales que se usan en un host compartido. + +
+ **Alertas:** -Configure reglas de alerta basadas en umbrales para metricas del host (CPU, memoria, disco, etc.) y reciba notificaciones a traves de ntfy o webhooks cuando se activen. Vea las alertas activas y resueltas en un historial de registros. +Pon reglas sobre mรฉtricas de host como CPU, memoria y disco, y recibe avisos por ntfy, Discord o un webhook cuando salten. Consulta las alertas activas y resueltas en un histรณrico y descarta las que no te importan. + + + +**Pรกgina de inicio:** +Una rejilla de widgets que montas tรบ mismo arrastrando y soltando. Hay widgets para el estado de los hosts, pings, enlaces a servicios, marcadores, bรบsqueda, relojes, calendarios, cuentas atrรกs, notas, RSS, tiempo, imรกgenes, iframes, Docker, tรบneles, grรกficas de mรฉtricas, APIs propias e incluso un terminal en vivo.
-**Pagina de Inicio:** -Una pagina de inicio completamente personalizable con una cuadricula de widgets de arrastrar y soltar. Agregue widgets para estado del host, enlaces de servicios, relojes, notas, feeds RSS, clima, contenedores Docker, graficos de metricas del host, terminales integrados, iframes y mas. +**Fragmentos y herramientas:** +Guarda los comandos que usas a menudo y lรกnzalos con un clic, con variables para el host y para lo que tรบ escribas. Ejecuta un mismo comando en todos los terminales abiertos y busca en tu historial con autocompletado. -**Cifrado de Base de Datos:** -Backend almacenado como archivos de base de datos SQLite cifrados. Consulte la [documentacion](https://docs.termix.site/security) para mas informacion. +**Compartir sesiรณn:** +Comparte en directo una sesiรณn de terminal, RDP, VNC o Telnet. Manda un enlace al que cualquiera puede entrar sin cuenta, o compรกrtela con un usuario concreto de Termix, en solo lectura o con escritura. Las comparticiones pueden caducar solas o revocarse, y se pueden desactivar globalmente o por host.
-**Grafico de Red:** -Personalice su Dashboard para visualizar su homelab basado en sus conexiones SSH con soporte de estado. +**Grabaciรณn y registros de sesiรณn:** +Graba sesiones de terminal, RDP y VNC y reprodรบcelas despuรฉs. Descarga registros de texto de una sesiรณn y mira el registro de conexiรณn para ver exactamente quรฉ pasรณ durante ella. -**Herramientas SSH:** -Cree fragmentos de comandos reutilizables que se ejecutan con un solo clic. Ejecute un comando simultaneamente en multiples terminales abiertos. +**Conexiones serie:** +Habla con dispositivos serie como routers, switches y microcontroladores desde el navegador o la aplicaciรณn de escritorio. Ajusta velocidad, bits de datos, bits de parada y paridad. Usa la API Web Serial en los navegadores compatibles, o un backend nativo en la aplicaciรณn de escritorio.
-**Pestanas Persistentes:** -Las sesiones SSH y pestanas permanecen abiertas entre dispositivos/actualizaciones si esta habilitado en el perfil de usuario. +**Tailscale:** +Trae dispositivos de tu tailnet para aรฑadirlos como hosts en un par de clics, y conรฉctate con Tailscale SSH para que las ACL de tu tailnet controlen el acceso sin guardar credenciales. Tambiรฉn funcionan Headscale y los puntos de acceso personalizados. + + + +**Proxmox:** +Importa hosts directamente desde una instancia de Proxmox y observa las estadรญsticas de nodos e invitados, incluidas CPU, memoria y almacenamiento, en su propia pestaรฑa. + +
+ +**Espacios de trabajo y pestaรฑas:** +Guarda un conjunto de pestaรฑas con su distribuciรณn dividida y reรกbrelo entero con un clic. Termix tambiรฉn recuerda tu รบltima sesiรณn, asรญ que tus pestaรฑas vuelven tras recargar y en otros dispositivos. + + + +**Configuraciรณn guiada:** +Una configuraciรณn corta te lleva por elegir un preajuste de interfaz, tu tema, las funciones que quieres y tu primer host. El modo sencillo esconde lo que no usas, y puedes repetir la configuraciรณn o cambiar de preajuste cuando quieras. + +
+ +**Escritorio independiente y sincronizaciรณn:** +La aplicaciรณn de escritorio funciona sola, con su backend y su base de datos locales, sin necesidad de servidor. Tambiรฉn puedes conectarla a un servidor Termix para sincronizar en ambos sentidos hosts, credenciales, fragmentos y mรกs, y decidir si las conexiones salen de tu equipo o pasan por el servidor. + + + +**Lรญnea de comandos:** +Un CLI `termix` para tu shell y tus scripts. Abre terminales, ejecuta un comando en un host o en una flota entera, mueve archivos por SFTP y gestiona hosts, fragmentos y credenciales. Instรกlalo con `npm install -g @termix-cli/cli` o coge un binario independiente. Consulta la [documentaciรณn del CLI](https://docs.termix.site/cli). + +
+ +**Seguridad:** +Las contraseรฑas, las claves y otros secretos se cifran por usuario, y los propios archivos de la base de datos se pueden cifrar en disco. Mira la [documentaciรณn](https://docs.termix.site/security) para saber cรณmo funciona. **Idiomas:** -Soporte integrado para aproximadamente 30 idiomas (gestionado por [Crowdin](https://docs.termix.site/translations)). +Unos 30 idiomas incluidos, gestionados a travรฉs de [Crowdin](https://docs.termix.site/translations).
- + - + @@ -250,11 +309,11 @@ Soporte integrado para aproximadamente 30 idiomas (gestionado por [Crowdin](http
-## Instalacion +## Instalaciรณn -Visite la [documentacion de Termix](https://docs.termix.site/install) para obtener instrucciones completas de instalacion en todas las plataformas. +Visita la [documentaciรณn de Termix](https://docs.termix.site/install) para ver las instrucciones completas de instalaciรณn en todas las plataformas. -Archivo de ejemplo de Docker Compose (puede omitir `guacd` y la red si no planea usar las funciones de escritorio remoto): +Ejemplo de archivo Docker Compose (puedes quitar `guacd` y la red si no piensas usar el escritorio remoto): ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### Lรญnea de comandos + +Termix tambiรฉn tiene un CLI, para que gestiones tus servidores desde un terminal y uses Termix en tus propios scripts. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +Puede abrir terminales, ejecutar un comando en un host o en una flota entera, mover archivos por SFTP y gestionar hosts, fragmentos y credenciales. La documentaciรณn completa estรก en [docs.termix.site/cli](https://docs.termix.site/cli). + +### Alojamiento en la nube + +Puedes ejecutar el servidor de Termix en un VPS en lugar de dentro de tu propia red. Si Termix corre en la red que gestiona, una caรญda se lo lleva por delante justo cuando lo necesitas para arreglar las cosas. Fuera se mantiene accesible, te da una IP fija y puedes entrar desde cualquier sitio sin VPN ni abrir puertos. + +[GINERNET](https://docs.termix.site/install/ginernet) patrocina Termix, y la documentaciรณn tiene una guรญa paso a paso para desplegar en su plataforma de VPS. + +
+ +## Telemetrรญa + +Termix envรญa una vez al dรญa un pequeรฑo aviso anรณnimo para que pueda ver cuรกntas instancias hay funcionando y quรฉ funciones se usan. Contiene un identificador de instancia aleatorio, cuรกntos usuarios y hosts tienes, la versiรณn de la aplicaciรณn y quรฉ funciones (terminal, gestor de archivos, tรบneles, docker, etc.) se han usado en las รบltimas 24 horas. Nunca contiene nombres de usuario, nombres de host, direcciones IP, credenciales ni nada que te identifique a ti o a tus servidores. + +Viene activado. Puedes desactivarlo en los ajustes de administraciรณn, en General, o poner `ENABLE_TELEMETRY=false` antes incluso de arrancar Termix. +
## Donar -Termix es gratuito y de codigo abierto, sin suscripciones ni planes de pago. Si lo encuentra util, considere donar para ayudar a cubrir los costos del servidor, los dominios y el tiempo de desarrollo. Las donaciones tambien ayudan a financiar el tiempo necesario para investigar y aprender lo que se necesita para construir funciones como soporte para SAML, Kubernetes y Agent. Siga el progreso y done a continuacion. +Termix es gratuito y de cรณdigo abierto, sin suscripciones ni planes de pago. Si te resulta รบtil, considera donar para ayudar con los servidores, los dominios y el tiempo de desarrollo. Las donaciones tambiรฉn financian el tiempo de investigar y aprender lo necesario para funciones como SAML, Kubernetes y el soporte de agentes. Sigue el progreso y dona abajo. [Donar](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix es gratuito y de codigo abierto, sin suscripciones ni planes de pago. Si ## Patrocinadores -Interesado en un espacio patrocinado de pago para apoyar el desarrollo? Escriba a [mail@termix.site](mailto:mail@termix.site). +ยฟTe interesa un espacio de pago para apoyar el desarrollo? Escribe a [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Interesado en un espacio patrocinado de pago para apoyar el desarrollo? Escriba Cloudflare     - - Tailscale - -    Akamai @@ -340,18 +421,21 @@ Interesado en un espacio patrocinado de pago para apoyar el desarrollo? Escriba Rack Genius - +    + + Ginernet +

## Soporte -Si necesita ayuda o desea solicitar una funcion para Termix, visite la pagina de [Issues](https://github.com/Termix-SSH/Support/issues), inicie sesion y pulse `New Issue`. Por favor, sea lo mas detallado posible en su reporte, preferiblemente escrito en ingles. Tambien puede unirse al servidor de [Discord](https://discord.gg/jVQGdvHDrf) y visitar el canal de soporte, sin embargo, los tiempos de respuesta pueden ser mas largos. +ยฟNecesitas ayuda o quieres pedir una funciรณn? Abre una [nueva incidencia](https://github.com/Termix-SSH/Support/issues) y aรฑade todo el detalle que puedas, en inglรฉs si te es posible. Tambiรฉn puedes preguntar en el canal de soporte de [Discord](https://discord.gg/jVQGdvHDrf), aunque allรญ las respuestas pueden tardar mรกs.
-## Capturas de Pantalla +## Capturas de pantalla
@@ -359,7 +443,7 @@ Si necesita ayuda o desea solicitar una funcion para Termix, visite la pagina de [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -Ver resรบmenes de actualizaciones en YouTube +Mira los resรบmenes de las actualizaciones en YouTube

@@ -399,18 +483,18 @@ Si necesita ayuda o desea solicitar una funcion para Termix, visite la pagina de
PlataformaDistribucionDistribuciรณn
WebCualquier navegador moderno (Chrome, Safari, Firefox) ยท Soporte PWACualquier navegador moderno (Chrome, Safari, Firefox) ยท Compatible con PWA
Windows x64/ia32
-Algunos videos e imagenes pueden estar desactualizados o no mostrar perfectamente las caracteristicas. +Algunos vรญdeos e imรกgenes pueden estar desactualizados o no mostrar del todo bien las funciones.
-## Caracteristicas Planeadas +## Caracterรญsticas planeadas -Consulte [Proyectos](https://github.com/orgs/Termix-SSH/projects/5) para todas las caracteristicas planeadas. Si desea contribuir, consulte [Contribuir](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +Todas las funciones planeadas estรกn en [Projects](https://github.com/orgs/Termix-SSH/projects/5). Si quieres colaborar, consulta [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
## Licencia -Distribuido bajo la Licencia Apache Version 2.0. Consulte `LICENSE` para mas informacion. +Distribuido bajo la Licencia Apache versiรณn 2.0. Consulta `LICENSE` para mรกs informaciรณn. diff --git a/docs/readme/README-FR.md b/docs/readme/README-FR.md index 0813165..571cb62 100644 --- a/docs/readme/README-FR.md +++ b/docs/readme/README-FR.md @@ -4,7 +4,7 @@

Termix

-

Gestion SSH auto-hebergee et acces bureau a distance

+

Gestion de serveurs auto-hรฉbergรฉe, du SSH au bureau ร  distance jusqu'aux automatisations

English ยท @@ -37,7 +37,7 @@
-Termix est gratuit et open source. Si vous le trouvez utile, pensez ร  [faire un don](https://donate.termix.site/) pour aider ร  couvrir les coรปts de serveur et le temps de dรฉveloppement. +Termix est gratuit et open source. S'il vous est utile, pensez ร  [faire un don](https://donate.termix.site/) pour aider ร  payer les serveurs et le temps de dรฉveloppement.
@@ -56,138 +56,194 @@ Termix est gratuit et open source. Si vous le trouvez utile, pensez ร  [faire un
-## Presentation +## Prรฉsentation -Termix est une plateforme de gestion de serveurs tout-en-un, open source, a jamais gratuite et auto-hebergee. Elle fournit une solution multiplateforme pour gerer vos serveurs et votre infrastructure a travers une interface unique et intuitive. Termix offre un acces terminal SSH, le controle de bureau a distance (RDP, VNC, Telnet), des capacites de tunneling SSH, la gestion de fichiers SSH a distance et de nombreux autres outils. Termix est l'alternative parfaite, gratuite et auto-hebergee a Termius, disponible sur toutes les plateformes. +Termix est une plateforme gratuite, open source et auto-hรฉbergรฉe pour gรฉrer vos serveurs. Elle rรฉunit au mรชme endroit les terminaux SSH, les bureaux ร  distance (RDP, VNC, Telnet), les transferts de fichiers, les tunnels, Docker, les mรฉtriques et les automatisations, sur le web, le bureau et le mobile. C'est une alternative auto-hรฉbergรฉe ร  Termius, gratuite pour toujours.
-## Fonctionnalites +## Fonctionnalitรฉs + + + - - - + + + + + + + + + + + + + + + + @@ -196,26 +252,29 @@ Support integre d'environ 30 langues (gere par [Crowdin](https://docs.termix.sit
-Plus de fonctionnalites +Plus de fonctionnalitรฉs
-- **Tableau de bord** - Consultez les informations de vos serveurs en un coup d'oeil depuis votre tableau de bord -- **Cles API** - Creez des cles API a portee utilisateur avec des dates d'expiration pour une utilisation en automatisation/CI -- **Export/Import de donnees** - Exportez et importez les hotes SSH, les identifiants et les donnees du gestionnaire de fichiers -- **Configuration SSL automatique** - Generation et gestion integrees de certificats SSL avec redirections HTTPS -- **Interface moderne** - Interface epuree compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez parmi de nombreux themes d'interface utilisateur, notamment clair, sombre, Dracula, etc. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein ecran. -- **Historique des commandes** - Auto-completion et consultation des commandes SSH precedemment executees -- **Connexion rapide** - Connectez-vous a un serveur sans avoir a sauvegarder les donnees de connexion -- **Palette de commandes** - Appuyez deux fois sur Shift gauche pour acceder rapidement aux connexions SSH avec votre clavier -- **Integration Proxmox** - Ajoutez automatiquement des hotes dans Termix depuis votre instance Proxmox -- **SSH riche en fonctionnalites** - Support des hotes de rebond, Warpgate, connexions basees sur TOTP, SOCKS5, verification des cles d'hote, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, journalisation du terminal, transfert d'agent SSH, agent SSH Bitwarden, signature SSH HashiCorp Vault, et plus encore. -- **Termix ID** - Un equivalent de sshid.io integre a Termix. Reservez un identifiant, publiez vos cles SSH publiques a une URL de resolution, et utilisez une autorite de certification integree pour emettre des certificats SSH. +- **Tableau de bord** - Vos serveurs en un coup d'ล“il, avec des cartes que vous rangez vous-mรชme +- **Graphe rรฉseau** - Votre homelab dessinรฉ ร  partir de vos hรดtes, avec l'รฉtat en direct +- **Moniteur tmux** - Parcourez les sessions, fenรชtres et panneaux tmux, avec aperรงus et recherche +- **Clรฉs API** - Des clรฉs par utilisateur avec date d'expiration, pour vos scripts et votre CI +- **Export et import** - Faites entrer et sortir hรดtes, identifiants et donnรฉes du gestionnaire de fichiers +- **SSL automatique** - Certificats gรฉnรฉrรฉs et renouvelรฉs pour vous, avec redirection HTTPS, ou apportez les vรดtres +- **Bases de donnรฉes** - SQLite par dรฉfaut, PostgreSQL et MySQL รฉgalement pris en charge +- **Interface moderne** - Une interface React soignรฉe qui marche sur ordinateur et mobile, avec des thรจmes clair, sombre et Dracula. Chaque connexion peut s'ouvrir en plein รฉcran depuis une URL +- **Palette de commandes** - Double appui sur Maj gauche pour rejoindre un hรดte au clavier +- **Raccourcis clavier** - Naviguer entre les onglets, les fermer et plus encore, tout est reconfigurable +- **Wake-on-LAN** - Rรฉveillez une machine depuis Termix ou depuis une รฉtape d'automatisation +- **Authentification par proxy de confiance** - Laissez un reverse proxy gรฉrer la connexion et transmettre l'utilisateur +- **SSH complet** - Hรดtes de rebond, Warpgate, demandes TOTP, SOCKS5, vรฉrification des clรฉs d'hรดte, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, journalisation du terminal, transfert d'agent, agent SSH Bitwarden, signature SSH HashiCorp Vault et plus encore +- **Termix ID** - Une version intรฉgrรฉe de sshid.io. Rรฉservez un identifiant, publiez vos clรฉs publiques sur une URL de rรฉsolution et รฉmettez des certificats SSH depuis l'autoritรฉ intรฉgrรฉe

-## Support des plateformes +## Plateformes prises en charge
-**Acces terminal SSH:** -Terminal complet avec support d'ecran partage (jusqu'a 4 panneaux) et un systeme d'onglets inspire des navigateurs. Inclut la personnalisation du terminal avec des themes courants, des polices et d'autres composants. +**Terminal SSH:** +Un vrai terminal avec des onglets faรงon navigateur et un รฉcran divisรฉ, jusqu'ร  6 panneaux ร  la fois. Choisissez votre thรจme, votre police et vos couleurs. Une barre d'outils au-dessus de chaque session affiche le CPU, la mรฉmoire et le disque en direct, avec des raccourcis vers les fichiers, Docker, les tunnels et les mรฉtriques de cet hรดte. -**Acces Bureau a Distance:** -Support RDP, VNC et Telnet via navigateur avec personnalisation complete et ecran partage. +**Bureau ร  distance:** +RDP, VNC et Telnet dans le navigateur, en onglets et en รฉcran divisรฉ comme n'importe quelle autre session. Comprend un explorateur de fichiers pour les lecteurs RDP et l'envoi par glisser-dรฉposer. Sur le bureau Windows, vous pouvez aussi ouvrir un hรดte dans le client RDP natif.
-**Gestion des tunnels SSH:** -Creez et gerez des tunnels SSH de serveur a serveur avec reconnexion automatique, surveillance de l'etat et transfert local, distant ou SOCKS dynamique. Les parametres de tunnel client-bureau-vers-serveur sont stockes localement par installation bureau ; des instantanes de prereglages C2S optionnels peuvent etre sauvegardes sur le serveur, renommes, charges ou supprimes pour deplacer une configuration de tunnel locale entre clients. +**Tunnels SSH:** +Redirection locale, distante et SOCKS dynamique, avec reconnexion automatique et vรฉrification de l'รฉtat. Les tunnels client vers serveur de l'application de bureau restent sur cette machine, et vous pouvez enregistrer des prรฉrรฉglages sur le serveur pour reprendre une configuration sur un autre poste. -**Gestionnaire de fichiers distant:** -Gerez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'edition de code, images, audio et video. Televersez, telechargez, renommez, supprimez et deplacez des fichiers de maniere fluide avec support sudo. Inclut la prise en charge du deplacement de fichiers de serveur a serveur. +**Gestionnaire de fichiers:** +Parcourez, modifiez, envoyez, tรฉlรฉchargez, renommez, dรฉplacez et supprimez des fichiers en SFTP, avec sudo. Affichez et modifiez du code, des images, de l'audio et de la vidรฉo. Copiez des fichiers directement d'un serveur ร  l'autre : le chemin le plus rapide est choisi pour vous et l'intรฉgritรฉ des transferts est vรฉrifiรฉe.
-**Gestion Docker et Podman:** -Demarrez, arretez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Controlez les conteneurs via le terminal docker exec. Compatible avec Docker et Podman comme environnement d'execution de conteneurs. Non concu pour remplacer Portainer ou Dockge, mais plutot pour gerer simplement vos conteneurs plutot que de les creer. +**Docker et Podman:** +Dรฉmarrez, arrรชtez, mettez en pause et supprimez des conteneurs, suivez leurs statistiques et ouvrez un shell ร  l'intรฉrieur. Fonctionne avec Docker comme avec Podman. Le but n'est pas de remplacer Portainer ou Dockge, juste de gรฉrer les conteneurs que vous avez dรฉjร . -**Gestionnaire d'hotes SSH:** -Enregistrez, organisez et gerez vos connexions SSH avec des tags et des dossiers (personnalisation des dossiers et prise en charge des dossiers imbriques), et sauvegardez facilement les informations de connexion reutilisables tout en automatisant le deploiement des cles SSH. +**Gestionnaire d'hรดtes:** +Rangez vos hรดtes avec des รฉtiquettes et des dossiers imbriquรฉs que vous pouvez nommer et colorer. Rรฉutilisez des identifiants enregistrรฉs sur plusieurs hรดtes, dรฉployez des clรฉs SSH automatiquement, regroupez des hรดtes sous un hรดte parent, modifiez et exportez en lot, et utilisez la connexion rapide pour les connexions ponctuelles que vous ne voulez pas garder.
-**Metriques d'hote:** -Visualisez l'utilisation du CPU, de la memoire, du disque, le reseau, le temps de fonctionnement, les informations systeme, le pare-feu, le moniteur de ports, le visualiseur de journaux, les utilisateurs/permissions, les certificats et bien plus encore sur la plupart des serveurs Linux. Inclut des graphiques d'historique en serie temporelle et des alertes basees sur des seuils avec support ntfy et webhook. +**Mรฉtriques des hรดtes:** +CPU, mรฉmoire, disque, rรฉseau, tempรฉrature, temps de fonctionnement, processus, ports, connexions et informations systรจme sur la plupart des serveurs Linux, avec des graphiques d'historique. Les cartes de gestion vous permettent de gรฉrer les services, les tรขches cron, les paquets, les utilisateurs, les rรจgles de pare-feu, WireGuard, Tailscale, les certificats SSL, les journaux et les vรฉrifications d'รฉtat sans quitter Termix. -**Authentification des utilisateurs:** -Gestion securisee des utilisateurs avec controles administrateur (peut modifier les informations des autres utilisateurs) et support OIDC/LDAP/SSO (avec controle d'acces), 2FA (TOTP), et support des passkeys (WebAuthn). Visualisez les sessions utilisateur actives sur toutes les plateformes et revoquez les permissions. Liez vos comptes OIDC/locaux ensemble. Consultez le journal d'audit des actions de tous les utilisateurs. +**Automatisations:** +Choisissez un dรฉclencheur, puis dites ce qui doit se passer. Les dรฉclencheurs peuvent รชtre une mรฉtrique qui dรฉpasse un seuil, un hรดte qui tombe ou revient, une vรฉrification d'รฉtat qui change, un horaire, un รฉvรฉnement de conteneur ou un webhook entrant. Les รฉtapes peuvent lancer des commandes et des extraits, piloter des conteneurs et des tunnels, rรฉveiller un hรดte, appeler une URL, attendre, se diviser selon une condition, lancer une autre automatisation et vous prรฉvenir via ntfy, Discord ou un webhook. Les essais ร  blanc vous permettent de tester sans risque.
-**Integration Tailscale:** -Listez les appareils de votre reseau Tailscale pour les ajouter rapidement comme hotes, et connectez-vous en utilisant Tailscale SSH comme methode d'authentification, laissant les ACL de votre reseau gerer l'autorisation sans stocker de credentials. +**Flottes:** +Regroupez des hรดtes dans une flotte en les choisissant ou avec des rรจgles d'รฉtiquettes, pour que les nouveaux hรดtes s'ajoutent tout seuls. Lancez une commande sur tous les hรดtes d'un coup, envoyez et rรฉcupรฉrez des fichiers sur l'ensemble, installez des paquets et collectez un inventaire de l'OS, du noyau, de l'architecture et du temps de fonctionnement. -**RBAC/Partage:** -Creez des roles et partagez des hotes entre utilisateurs/roles. Prend en charge tous les types d'authentification et tous les protocoles d'hote. +**Assistant IA:** +Optionnel, et dรฉsactivรฉ tant que vous ne l'activez pas. Connectez OpenAI, Anthropic, Gemini, Ollama ou n'importe quel point d'accรจs compatible OpenAI et posez des questions sur votre installation. Il lit les hรดtes, les flottes, les extraits et les alertes, et propose des changements que vous validez au lieu de les appliquer lui-mรชme. Il ne peut jamais toucher aux identifiants, aux utilisateurs ni aux rรฉglages. Les administrateurs peuvent le laisser dรฉsactivรฉ pour toute l'instance, et vous pouvez le masquer pendant la configuration.
-**Connexions Serie:** -Connectez-vous a des appareils serie (routeurs, commutateurs, microcontroleurs, etc.) directement depuis le navigateur ou l'application bureau. Configurez le debit en bauds, les bits de donnees, les bits d'arret et la parite. Utilise l'API Web Serial dans les navigateurs compatibles ou un backend natif dans l'application Electron. +**Connexion et utilisateurs:** +Comptes locaux ainsi que connexion OIDC, LDAP, GitHub et Google, avec double authentification (TOTP), clรฉs d'accรจs (WebAuthn) et appareils de confiance. Les administrateurs peuvent gรฉrer les utilisateurs, associer les groupes OIDC aux rรดles, voir toutes les sessions actives sur toutes les plateformes et les rรฉvoquer. Reliez vos comptes local et OIDC, et consultez le journal d'audit de ce que chacun a fait. +**Rรดles et partage:** +Crรฉez des rรดles et partagez des hรดtes avec des utilisateurs ou des rรดles selon quatre niveaux : connexion, lecture, modification et gestion. Cela fonctionne avec tous les types d'authentification et tous les protocoles, et vous pouvez remplacer les identifiants utilisรฉs pour un hรดte partagรฉ. + +
+ **Alertes:** -Definissez des regles d'alerte basees sur des seuils pour les metriques d'hote (CPU, memoire, disque, etc.) et recevez des notifications via ntfy ou webhooks lorsqu'elles se declenchent. Consultez les alertes actives et resolues dans un journal d'historique. +Dรฉfinissez des rรจgles sur les mรฉtriques des hรดtes comme le CPU, la mรฉmoire et le disque, et recevez une notification via ntfy, Discord ou un webhook quand elles se dรฉclenchent. Consultez les alertes en cours et rรฉsolues dans un historique, et รฉcartez celles qui ne vous intรฉressent pas.
**Page d'accueil:** -Une page d'accueil entierement personnalisable avec une grille de widgets glisser-deposer. Ajoutez des widgets pour l'etat des hotes, les liens de services, les horloges, les notes, les flux RSS, la meteo, les conteneurs Docker, les graphiques de metriques d'hote, les terminaux integres, les iframes et plus encore. - - - -**Chiffrement de la base de donnees:** -Le backend est stocke sous forme de fichiers de base de donnees SQLite chiffres. Consultez la [documentation](https://docs.termix.site/security) pour plus de details. +Une grille de widgets en glisser-dรฉposer que vous construisez vous-mรชme. Des widgets pour l'รฉtat des hรดtes, les pings, les liens de services, les favoris, la recherche, les horloges, les calendriers, les comptes ร  rebours, les notes, les flux RSS, la mรฉtรฉo, les images, les iframes, Docker, les tunnels, les graphiques de mรฉtriques, les API personnalisรฉes et mรชme un terminal en direct.
-**Graphe reseau:** -Personnalisez votre tableau de bord pour visualiser votre homelab base sur vos connexions SSH avec support des statuts. +**Extraits et outils:** +Enregistrez les commandes que vous lancez souvent et exรฉcutez-les en un clic, avec des variables pour l'hรดte et vos propres saisies. Lancez une mรชme commande dans tous les terminaux ouverts, et cherchez dans votre historique avec la complรฉtion automatique. -**Outils SSH:** -Creez des extraits de commandes reutilisables executables en un seul clic. Executez une commande simultanement sur plusieurs terminaux ouverts. +**Partage de session:** +Partagez en direct une session terminal, RDP, VNC ou Telnet. Envoyez un lien que n'importe qui peut rejoindre sans compte, ou partagez avec un utilisateur Termix prรฉcis, en lecture seule ou en lecture-รฉcriture. Les partages peuvent expirer d'eux-mรชmes ou รชtre rรฉvoquรฉs, et se dรฉsactivent globalement ou hรดte par hรดte.
-**Onglets Persistants:** -Les sessions SSH et les onglets restent ouverts sur tous les appareils/actualisations si active dans le profil utilisateur. +**Enregistrement et journaux de session:** +Enregistrez les sessions terminal, RDP et VNC pour les revoir plus tard. Tรฉlรฉchargez les journaux d'une session en texte simple, et consultez le journal de connexion pour voir exactement ce qui s'est passรฉ pendant une connexion. + + + +**Connexions sรฉrie:** +Dialoguez avec des appareils sรฉrie comme des routeurs, des commutateurs et des microcontrรดleurs depuis le navigateur ou l'application de bureau. Rรฉglez la vitesse, les bits de donnรฉes, les bits d'arrรชt et la paritรฉ. Utilise l'API Web Serial dans les navigateurs compatibles, ou un backend natif dans l'application de bureau. + +
+ +**Tailscale:** +Rรฉcupรฉrez les appareils de votre tailnet pour les ajouter comme hรดtes en quelques clics, et connectez-vous avec Tailscale SSH pour que les ACL de votre tailnet gรจrent les accรจs, sans stocker d'identifiants. Headscale et les points d'accรจs personnalisรฉs fonctionnent aussi. + + + +**Proxmox:** +Importez des hรดtes directement depuis une instance Proxmox, et suivez les statistiques des nล“uds et des invitรฉs, dont le CPU, la mรฉmoire et le stockage, dans un onglet dรฉdiรฉ. + +
+ +**Espaces de travail et onglets:** +Enregistrez un ensemble d'onglets avec leur disposition en รฉcran divisรฉ et rouvrez le tout en un clic. Termix retient aussi votre derniรจre session, donc vos onglets reviennent aprรจs un rafraรฎchissement ou sur un autre appareil. + + + +**Configuration guidรฉe:** +Une courte configuration vous aide ร  choisir un prรฉrรฉglage d'interface, votre thรจme, les fonctionnalitรฉs que vous voulez et votre premier hรดte. Le mode simple masque ce que vous n'utilisez pas, et vous pouvez relancer la configuration ou changer de prรฉrรฉglage quand vous voulez. + +
+ +**Application de bureau autonome et synchronisation:** +L'application de bureau fonctionne toute seule, avec son propre backend et sa base de donnรฉes, sans serveur. Vous pouvez aussi la relier ร  un serveur Termix pour synchroniser dans les deux sens les hรดtes, les identifiants, les extraits et le reste, et choisir si les connexions partent de votre machine ou passent par le serveur. + + + +**Ligne de commande:** +Un CLI `termix` pour votre shell et vos scripts. Ouvrez des terminaux, lancez une commande sur un hรดte ou une flotte entiรจre, dรฉplacez des fichiers en SFTP et gรฉrez hรดtes, extraits et identifiants. Installez-le avec `npm install -g @termix-cli/cli` ou rรฉcupรฉrez un binaire autonome. Voir la [documentation du CLI](https://docs.termix.site/cli). + +
+ +**Sรฉcuritรฉ:** +Les mots de passe, les clรฉs et les autres secrets sont chiffrรฉs par utilisateur, et les fichiers de base de donnรฉes eux-mรชmes peuvent รชtre chiffrรฉs sur le disque. Voir la [documentation](https://docs.termix.site/security) pour le dรฉtail. **Langues:** -Support integre d'environ 30 langues (gere par [Crowdin](https://docs.termix.site/translations)). +Une trentaine de langues intรฉgrรฉes, gรฉrรฉes via [Crowdin](https://docs.termix.site/translations).
@@ -224,11 +283,11 @@ Support integre d'environ 30 langues (gere par [Crowdin](https://docs.termix.sit - + - + @@ -252,9 +311,9 @@ Support integre d'environ 30 langues (gere par [Crowdin](https://docs.termix.sit ## Installation -Visitez la [documentation](https://docs.termix.site/install) de Termix pour des instructions d'installation completes sur toutes les plateformes. +Consultez la [documentation Termix](https://docs.termix.site/install) pour les instructions d'installation complรจtes sur toutes les plateformes. -Voici un exemple de fichier Docker Compose (vous pouvez omettre guacd et le reseau si vous ne prevoyez pas d'utiliser les fonctionnalites de bureau a distance) : +Exemple de fichier Docker Compose (vous pouvez retirer `guacd` et le rรฉseau si vous ne comptez pas utiliser le bureau ร  distance) : ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### Ligne de commande + +Termix propose aussi un CLI, pour gรฉrer vos serveurs depuis un terminal et utiliser Termix dans vos propres scripts. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +Il peut ouvrir des terminaux, lancer une commande sur un hรดte ou une flotte entiรจre, dรฉplacer des fichiers en SFTP et gรฉrer hรดtes, extraits et identifiants. La documentation complรจte est sur [docs.termix.site/cli](https://docs.termix.site/cli). + +### Hรฉbergement cloud + +Vous pouvez faire tourner le serveur Termix sur un VPS plutรดt que dans votre propre rรฉseau. Si Termix tourne sur le rรฉseau qu'il gรจre, une panne l'emporte avec elle, juste au moment oรน vous en avez besoin pour rรฉparer. Ailleurs, il reste joignable, vous avez une IP fixe et vous pouvez y accรฉder de partout sans VPN ni redirection de port. + +[GINERNET](https://docs.termix.site/install/ginernet) sponsorise Termix, et la documentation contient un guide pas ร  pas pour dรฉployer sur leur plateforme VPS. + +
+ +## Tรฉlรฉmรฉtrie + +Termix envoie une fois par jour un petit signal anonyme, pour que je puisse voir combien d'instances tournent et quelles fonctionnalitรฉs servent vraiment. Il contient un identifiant d'instance alรฉatoire, le nombre d'utilisateurs et d'hรดtes, la version de l'application et les fonctionnalitรฉs utilisรฉes ces derniรจres 24 heures (terminal, gestionnaire de fichiers, tunnels, docker, etc.). Il ne contient jamais de noms d'utilisateur, de noms d'hรดtes, d'adresses IP, d'identifiants ni quoi que ce soit qui puisse vous identifier, vous ou vos serveurs. + +C'est activรฉ par dรฉfaut. Dรฉsactivez-le dans les paramรจtres d'administration, section Gรฉnรฉral, ou dรฉfinissez `ENABLE_TELEMETRY=false` avant mรชme de dรฉmarrer Termix. +
## Faire un don -Termix est gratuit et open source, sans abonnement ni plan payant. Si vous le trouvez utile, pensez a faire un don pour aider a couvrir les couts de serveur, les domaines et le temps de developpement. Les dons contribuent egalement a financer le temps necessaire pour rechercher et apprendre ce qui est requis pour construire des fonctionnalites comme SAML, Kubernetes et le support des agents. Suivez la progression et faites un don ci-dessous. +Termix est gratuit et open source, sans abonnement ni offre payante. S'il vous est utile, pensez ร  faire un don pour aider ร  couvrir les serveurs, les noms de domaine et le temps de dรฉveloppement. Les dons financent aussi le temps de recherche nรฉcessaire pour construire des fonctionnalitรฉs comme SAML, Kubernetes et le support des agents. Suivez l'avancement et faites un don ci-dessous. [Faire un don](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix est gratuit et open source, sans abonnement ni plan payant. Si vous le tr ## Sponsors -Interesse par un placement payant pour soutenir le developpement ? Envoyez un email a [mail@termix.site](mailto:mail@termix.site). +Intรฉressรฉ par un emplacement payant pour soutenir le dรฉveloppement ? ร‰crivez ร  [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Interesse par un placement payant pour soutenir le developpement ? Envoyez un em Cloudflare     - - Tailscale - -    Akamai @@ -340,18 +421,21 @@ Interesse par un placement payant pour soutenir le developpement ? Envoyez un em Rack Genius - +    + + Ginernet +

## Support -Si vous avez besoin d'aide ou souhaitez demander une fonctionnalite pour Termix, visitez la page [Issues](https://github.com/Termix-SSH/Support/issues), connectez-vous et appuyez sur `New Issue`. Veuillez etre aussi detaille que possible dans votre issue, de preference redigee en anglais. Vous pouvez egalement rejoindre le serveur [Discord](https://discord.gg/jVQGdvHDrf) et visiter le canal de support, cependant les temps de reponse peuvent etre plus longs. +Besoin d'aide ou envie de proposer une fonctionnalitรฉ ? Ouvrez un [nouveau ticket](https://github.com/Termix-SSH/Support/issues) avec le plus de dรฉtails possible, en anglais si vous le pouvez. Vous pouvez aussi demander dans le canal support sur [Discord](https://discord.gg/jVQGdvHDrf), mรชme si les rรฉponses y prennent parfois plus de temps.
-## Captures d'ecran +## Captures d'รฉcran
@@ -359,7 +443,7 @@ Si vous avez besoin d'aide ou souhaitez demander une fonctionnalite pour Termix, [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -Regarder les aperรงus des mises a jour sur YouTube +Regarder les prรฉsentations des mises ร  jour sur YouTube

@@ -399,18 +483,18 @@ Si vous avez besoin d'aide ou souhaitez demander une fonctionnalite pour Termix,
WebTout navigateur moderne (Chrome, Safari, Firefox) ยท Support PWATout navigateur rรฉcent (Chrome, Safari, Firefox) ยท Compatible PWA
Windows x64/ia32Portable ยท MSI Installateur ยท ChocolateyPortable ยท Installeur MSI ยท Chocolatey
Linux x64/ia32
-Certaines videos et images peuvent etre obsoletes ou ne pas presenter parfaitement les fonctionnalites. +Certaines vidรฉos et images peuvent รชtre dรฉpassรฉes ou ne pas montrer parfaitement les fonctionnalitรฉs.
-## Fonctionnalites prevues +## Fonctionnalitรฉs prรฉvues -Consultez les [Projects](https://github.com/orgs/Termix-SSH/projects/5) pour toutes les fonctionnalites prevues. Si vous souhaitez contribuer, consultez [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +Toutes les fonctionnalitรฉs prรฉvues sont dans [Projects](https://github.com/orgs/Termix-SSH/projects/5). Si vous souhaitez contribuer, voir [Contribuer](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
## Licence -Distribue sous la licence Apache Version 2.0. Consultez `LICENSE` pour plus d'informations. +Distribuรฉ sous licence Apache version 2.0. Voir `LICENSE` pour plus d'informations. diff --git a/docs/readme/README-HI.md b/docs/readme/README-HI.md index 17a1e63..ae6f0d4 100644 --- a/docs/readme/README-HI.md +++ b/docs/readme/README-HI.md @@ -4,7 +4,7 @@

Termix

-

เคธเฅเคต-เคนเฅ‹เคธเฅเคŸเฅ‡เคก SSH เคชเฅเคฐเคฌเค‚เคงเคจ เค”เคฐ เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเค•เฅเคธเฅ‡เคธ

+

เคธเฅ‡เคฒเฅเคซ-เคนเฅ‹เคธเฅเคŸเฅ‡เคก เคธเคฐเฅเคตเคฐ เคชเฅเคฐเคฌเค‚เคงเคจ, SSH เค”เคฐ เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคธเฅ‡ เคฒเฅ‡เค•เคฐ เค‘เคŸเฅ‹เคฎเฅ‡เคถเคจ เคคเค•

English ยท @@ -37,7 +37,7 @@
-Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆเฅค เคฏเคฆเคฟ เค†เคชเค•เฅ‹ เคฏเคน เค‰เคชเคฏเฅ‹เค—เฅ€ เคฒเค—เคคเคพ เคนเฅˆ, เคคเฅ‹ เคธเคฐเฅเคตเคฐ เคฒเคพเค—เคค เค”เคฐ เคตเคฟเค•เคพเคธ เคธเคฎเคฏ เคฎเฅ‡เค‚ เคฎเคฆเคฆ เค•เฅ‡ เคฒเคฟเค [เคฆเคพเคจ เค•เคฐเฅ‡เค‚](https://donate.termix.site/)เฅค +Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆเฅค เค…เค—เคฐ เคฏเคน เค†เคชเค•เฅ‡ เค•เคพเคฎ เค†เคคเคพ เคนเฅˆ, เคคเฅ‹ เคธเคฐเฅเคตเคฐ เค•เฅ€ เคฒเคพเค—เคค เค”เคฐ เคตเคฟเค•เคพเคธ เค•เฅ‡ เคธเคฎเคฏ เคฎเฅ‡เค‚ เคฎเคฆเคฆ เค•เฅ‡ เคฒเคฟเค [เคฆเคพเคจ](https://donate.termix.site/) เค•เคฐเคจเฅ‡ เคชเคฐ เคตเคฟเคšเคพเคฐ เค•เคฐเฅ‡เค‚เฅค
@@ -49,7 +49,7 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆเฅค เคฏเคฆเคฟ

Repo of the Day Achievement
- 1 เคธเคฟเคคเค‚เคฌเคฐ, 2025 เค•เฅ‹ เคชเฅเคฐเคพเคชเฅเคค + 1 เคธเคฟเคคเค‚เคฌเคฐ 2025 เค•เฅ‹ เคนเคพเคธเคฟเคฒ เค•เคฟเคฏเคพ เค—เคฏเคพ

@@ -58,7 +58,7 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆเฅค เคฏเคฆเคฟ ## เค…เคตเคฒเฅ‹เค•เคจ -Termix เคเค• เค“เคชเคจ-เคธเฅ‹เคฐเฅเคธ, เคนเคฎเฅ‡เคถเคพ เค•เฅ‡ เคฒเคฟเค เคฎเฅเคซเคผเฅเคค, เคธเฅ‡เคฒเฅเคซ-เคนเฅ‹เคธเฅเคŸเฅ‡เคก เค‘เคฒ-เค‡เคจ-เคตเคจ เคธเคฐเฅเคตเคฐ เคชเฅเคฐเคฌเค‚เคงเคจ เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคนเฅˆเฅค เคฏเคน เคเค• เคเค•เคฒ, เคธเคนเคœ เค‡เค‚เคŸเคฐเคซเคผเฅ‡เคธ เค•เฅ‡ เคฎเคพเคงเฅเคฏเคฎ เคธเฅ‡ เค†เคชเค•เฅ‡ เคธเคฐเฅเคตเคฐ เค”เคฐ เคฌเฅเคจเคฟเคฏเคพเคฆเฅ€ เคขเคพเคเคšเฅ‡ เค•เฅ‡ เคชเฅเคฐเคฌเค‚เคงเคจ เค•เฅ‡ เคฒเคฟเค เคเค• เคฎเคฒเฅเคŸเฅ€-เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคธเคฎเคพเคงเคพเคจ เคชเฅเคฐเคฆเคพเคจ เค•เคฐเคคเคพ เคนเฅˆเฅค Termix SSH เคŸเคฐเฅเคฎเคฟเคจเคฒ เคเค•เฅเคธเฅ‡เคธ, เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เค•เค‚เคŸเฅเคฐเฅ‹เคฒ (RDP, VNC, Telnet), SSH เคŸเคจเคฒเคฟเค‚เค— เค•เฅเคทเคฎเคคเคพเคเค, เคฐเคฟเคฎเฅ‹เคŸ เคซเคผเคพเค‡เคฒ เคชเฅเคฐเคฌเค‚เคงเคจ, เค”เคฐ เค•เคˆ เค…เคจเฅเคฏ เค‰เคชเค•เคฐเคฃ เคชเฅเคฐเคฆเคพเคจ เค•เคฐเคคเคพ เคนเฅˆเฅค Termix เคธเคญเฅ€ เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคชเคฐ เค‰เคชเคฒเคฌเฅเคง Termius เค•เคพ เคธเคนเฅ€ เคฎเฅเคซเคผเฅเคค เค”เคฐ เคธเฅ‡เคฒเฅเคซ-เคนเฅ‹เคธเฅเคŸเฅ‡เคก เคตเคฟเค•เคฒเฅเคช เคนเฅˆเฅค +Termix เค†เคชเค•เฅ‡ เคธเคฐเฅเคตเคฐ เคธเค‚เคญเคพเคฒเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคเค• เคฎเฅเคซเคผเฅเคค, เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ, เคธเฅ‡เคฒเฅเคซ-เคนเฅ‹เคธเฅเคŸเฅ‡เคก เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคนเฅˆเฅค เคฏเคน SSH เคŸเคฐเฅเคฎเคฟเคจเคฒ, เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช (RDP, VNC, Telnet), เคซเคผเคพเค‡เคฒ เคŸเฅเคฐเคพเค‚เคธเคซเคผเคฐ, เคŸเคจเคฒ, Docker, เคฎเฅ‡เคŸเฅเคฐเคฟเค•เฅเคธ เค”เคฐ เค‘เคŸเฅ‹เคฎเฅ‡เคถเคจ เค•เฅ‹ เคเค• เคนเฅ€ เคœเค—เคน เคฒเคพเคคเคพ เคนเฅˆ, เคตเฅ‡เคฌ, เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เค”เคฐ เคฎเฅ‹เคฌเคพเค‡เคฒ เคชเคฐเฅค เคฏเคน Termius เค•เคพ เคธเฅ‡เคฒเฅเคซ-เคนเฅ‹เคธเฅเคŸเฅ‡เคก เคตเคฟเค•เคฒเฅเคช เคนเฅˆ เคœเฅ‹ เคนเคฎเฅ‡เคถเคพ เคฎเฅเคซเคผเฅเคค เคฐเคนเฅ‡เค—เคพเฅค
@@ -68,42 +68,42 @@ Termix เคเค• เค“เคชเคจ-เคธเฅ‹เคฐเฅเคธ, เคนเคฎเฅ‡เคถเคพ เค•เฅ‡ เคฒเคฟเค เคฎเฅ -**SSH เคŸเคฐเฅเคฎเคฟเคจเคฒ เคเค•เฅเคธเฅ‡เคธ:** -เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคœเฅˆเคธเฅ€ เคŸเฅˆเคฌ เคชเฅเคฐเคฃเคพเคฒเฅ€ เค•เฅ‡ เคธเคพเคฅ เคธเฅเคชเฅเคฒเคฟเคŸ-เคธเฅเค•เฅเคฐเฅ€เคจ เคธเคชเฅ‹เคฐเฅเคŸ (4 เคชเฅˆเคจเคฒ เคคเค•) เคตเคพเคฒเคพ เคชเฅ‚เคฐเฅเคฃ-เคตเคฟเคถเฅ‡เคทเคคเคพ เคตเคพเคฒเคพ เคŸเคฐเฅเคฎเคฟเคจเคฒเฅค เค‡เคธเคฎเฅ‡เค‚ เคฒเฅ‹เค•เคชเฅเคฐเคฟเคฏ เคŸเคฐเฅเคฎเคฟเคจเคฒ เคฅเฅ€เคฎ, เคซเคผเฅ‰เคจเฅเคŸ เค”เคฐ เค…เคจเฅเคฏ เค•เค‚เคชเฅ‹เคจเฅ‡เค‚เคŸ เคธเคนเคฟเคค เคŸเคฐเฅเคฎเคฟเคจเคฒ เค•เฅ‹ เค•เคธเฅเคŸเคฎเคพเค‡เคœเคผ เค•เคฐเคจเฅ‡ เค•เคพ เคธเคชเฅ‹เคฐเฅเคŸ เคถเคพเคฎเคฟเคฒ เคนเฅˆเฅค +**SSH เคŸเคฐเฅเคฎเคฟเคจเคฒ:** +เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคœเฅˆเคธเฅ‡ เคŸเฅˆเคฌ เค”เคฐ เคธเฅเคชเฅเคฒเคฟเคŸ เคธเฅเค•เฅเคฐเฅ€เคจ เคตเคพเคฒเคพ เคชเฅ‚เคฐเคพ เคŸเคฐเฅเคฎเคฟเคจเคฒ, เคเค• เคธเคพเคฅ 6 เคชเฅˆเคจเคฒ เคคเค•เฅค เคฅเฅ€เคฎ, เคซเคผเฅ‰เคจเฅเคŸ เค”เคฐ เคฐเค‚เค— เค†เคช เค–เฅเคฆ เคšเฅเคจเฅ‡เค‚เฅค เคนเคฐ เคธเคคเฅเคฐ เค•เฅ‡ เคŠเคชเคฐ เคเค• เคŸเฅ‚เคฒเคฌเคพเคฐ เคฐเคนเคคเคพ เคนเฅˆ เคœเคฟเคธเคฎเฅ‡เค‚ CPU, เคฎเฅ‡เคฎเฅ‹เคฐเฅ€ เค”เคฐ เคกเคฟเคธเฅเค• เคฒเคพเค‡เคต เคฆเคฟเค–เคคเฅ‡ เคนเฅˆเค‚, เคธเคพเคฅ เคนเฅ€ เค‰เคธ เคนเฅ‹เคธเฅเคŸ เค•เฅ€ เคซเคผเคพเค‡เคฒเฅ‹เค‚, Docker, เคŸเคจเคฒ เค”เคฐ เคฎเฅ‡เคŸเฅเคฐเคฟเค•เฅเคธ เคคเค• เคœเคพเคจเฅ‡ เค•เฅ‡ เคถเฅ‰เคฐเฅเคŸเค•เคŸ เคญเฅ€เฅค -**เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเค•เฅเคธเฅ‡เคธ:** -เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคชเคฐ RDP, VNC เค”เคฐ Telnet เคธเคชเฅ‹เคฐเฅเคŸ, เคชเฅ‚เคฐเฅเคฃ เค•เคธเฅเคŸเคฎเคพเค‡เคœเคผเฅ‡เคถเคจ เค”เคฐ เคธเฅเคชเฅเคฒเคฟเคŸ เคธเฅเค•เฅเคฐเฅ€เคจ เค•เฅ‡ เคธเคพเคฅเฅค +**เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช:** +เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคฎเฅ‡เค‚ RDP, VNC เค”เคฐ Telnet, เคฌเคพเค•เฅ€ เคธเคคเฅเคฐเฅ‹เค‚ เค•เฅ€ เคคเคฐเคน เคŸเฅˆเคฌ เค”เคฐ เคธเฅเคชเฅเคฒเคฟเคŸ เคธเฅเค•เฅเคฐเฅ€เคจ เคฎเฅ‡เค‚เฅค เค‡เคธเคฎเฅ‡เค‚ RDP เคกเฅเคฐเคพเค‡เคต เค•เฅ‡ เคฒเคฟเค เคซเคผเคพเค‡เคฒ เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เค”เคฐ เค–เฅ€เค‚เคšเค•เคฐ เค›เฅ‹เคกเคผเคจเฅ‡ เคตเคพเคฒเคพ เค…เคชเคฒเฅ‹เคก เคญเฅ€ เคนเฅˆเฅค Windows เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคชเคฐ เค†เคช เคนเฅ‹เคธเฅเคŸ เค•เฅ‹ เคธเคฟเคธเฅเคŸเคฎ เค•เฅ‡ เค…เคชเคจเฅ‡ RDP เค•เฅเคฒเคพเค‡เค‚เคŸ เคฎเฅ‡เค‚ เคญเฅ€ เค–เฅ‹เคฒ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค -**SSH เคŸเคจเคฒ เคชเฅเคฐเคฌเค‚เคงเคจ:** -เค‘เคŸเฅ‹เคฎเฅˆเคŸเคฟเค• เคฐเฅ€เค•เคจเฅ‡เค•เฅเคถเคจ, เคนเฅ‡เคฒเฅเคฅ เคฎเฅ‰เคจเคฟเคŸเคฐเคฟเค‚เค— เค”เคฐ เคฒเฅ‹เค•เคฒ, เคฐเคฟเคฎเฅ‹เคŸ เคฏเคพ เคกเคพเคฏเคจเฅ‡เคฎเคฟเค• SOCKS เคซเฅ‰เคฐเคตเคฐเฅเคกเคฟเค‚เค— เค•เฅ‡ เคธเคพเคฅ เคธเคฐเฅเคตเคฐ-เคŸเฅ-เคธเคฐเฅเคตเคฐ SSH เคŸเคจเคฒ เคฌเคจเคพเคเค เค”เคฐ เคชเฅเคฐเคฌเค‚เคงเคฟเคค เค•เคฐเฅ‡เค‚เฅค เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เค•เฅเคฒเคพเค‡เค‚เคŸ-เคŸเฅ-เคธเคฐเฅเคตเคฐ เคŸเคจเคฒ เคธเฅ‡เคŸเคฟเค‚เค—เฅเคธ เคชเฅเคฐเคคเฅเคฏเฅ‡เค• เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เค‡เค‚เคธเฅเคŸเฅ‰เคฒ เคฎเฅ‡เค‚ เคธเฅเคฅเคพเคจเฅ€เคฏ เคฐเฅ‚เคช เคธเฅ‡ เคธเค‚เค—เฅเคฐเคนเฅ€เคค เคนเฅ‹เคคเฅ€ เคนเฅˆเค‚; เคตเฅˆเค•เคฒเฅเคชเคฟเค• C2S เคชเฅเคฐเฅ€เคธเฅ‡เคŸ เคธเฅเคจเฅˆเคชเคถเฅ‰เคŸ เคธเคฐเฅเคตเคฐ เคชเคฐ เคธเฅ‡เคต เค•เคฟเค เคœเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚, เคคเคฅเคพ เคœเคฌ เค†เคช เค•เคฟเคธเฅ€ เคฒเฅ‹เค•เคฒ เคŸเคจเคฒ เค•เฅ‰เคจเฅเคซเคผเคฟเค—เคฐเฅ‡เคถเคจ เค•เฅ‹ เค•เฅเคฒเคพเค‡เค‚เคŸ เค•เฅ‡ เคฌเฅ€เคš เคธเฅเคฅเคพเคจเคพเค‚เคคเคฐเคฟเคค เค•เคฐเคจเคพ เคšเคพเคนเฅ‡เค‚ เคคเฅ‹ เค‰เคจเฅเคนเฅ‡เค‚ เคฐเฅ€เคจเฅ‡เคฎ, เคฒเฅ‹เคก เคฏเคพ เคกเคฟเคฒเฅ€เคŸ เค•เคฟเคฏเคพ เคœเคพ เคธเค•เคคเคพ เคนเฅˆเฅค +**SSH เคŸเคจเคฒ:** +เคฒเฅ‹เค•เคฒ, เคฐเคฟเคฎเฅ‹เคŸ เค”เคฐ เคกเคพเคฏเคจเคพเคฎเคฟเค• SOCKS เคซเคผเฅ‰เคฐเคตเคฐเฅเคกเคฟเค‚เค—, เค…เคชเคจเฅ‡ เค†เคช เคฆเฅ‹เคฌเคพเคฐเคพ เคœเฅเคกเคผเคจเฅ‡ เค”เคฐ เคธเฅเคฅเคฟเคคเคฟ เคœเคพเคเคš เค•เฅ‡ เคธเคพเคฅเฅค เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเคช เค•เฅ‡ เค•เฅเคฒเคพเค‡เค‚เคŸ เคธเฅ‡ เคธเคฐเฅเคตเคฐ เคตเคพเคฒเฅ‡ เคŸเคจเคฒ เค‰เคธเฅ€ เคฎเคถเฅ€เคจ เคชเคฐ เคฐเคนเคคเฅ‡ เคนเฅˆเค‚, เค”เคฐ เค†เคช เคธเฅ‡เคŸเคฟเค‚เค— เคธเคฐเฅเคตเคฐ เคชเคฐ เคธเคนเฅ‡เคœเค•เคฐ เค‰เคธเฅ‡ เคฆเฅ‚เคธเคฐเฅ€ เคฎเคถเฅ€เคจ เคชเคฐ เคฒเฅ‡ เคœเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค -**เคฐเคฟเคฎเฅ‹เคŸ เคซเคผเคพเค‡เคฒ เคฎเฅˆเคจเฅ‡เคœเคฐ:** -เค•เฅ‹เคก, เค‡เคฎเฅ‡เคœ, เค‘เคกเคฟเคฏเฅ‹ เค”เคฐ เคตเฅ€เคกเคฟเคฏเฅ‹ เคฆเฅ‡เค–เคจเฅ‡ เค”เคฐ เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเคจเฅ‡ เค•เฅ‡ เคธเคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคธเคพเคฅ เคฐเคฟเคฎเฅ‹เคŸ เคธเคฐเฅเคตเคฐ เคชเคฐ เคธเฅ€เคงเฅ‡ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคชเฅเคฐเคฌเค‚เคงเคฟเคค เค•เคฐเฅ‡เค‚เฅค sudo เคธเคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคธเคพเคฅ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เค…เคชเคฒเฅ‹เคก, เคกเคพเค‰เคจเคฒเฅ‹เคก, เคฐเฅ€เคจเฅ‡เคฎ, เคกเคฟเคฒเฅ€เคŸ เค”เคฐ เคฎเฅ‚เคต เค•เคฐเฅ‡เค‚เฅค เค‡เคธเคฎเฅ‡เค‚ เคซเคผเคพเค‡เคฒเฅ‹เค‚ เค•เฅ‹ เคเค• เคธเคฐเฅเคตเคฐ เคธเฅ‡ เคฆเฅ‚เคธเคฐเฅ‡ เคธเคฐเฅเคตเคฐ เคฎเฅ‡เค‚ เคธเฅเคฅเคพเคจเคพเค‚เคคเคฐเคฟเคค เค•เคฐเคจเฅ‡ เค•เคพ เคธเคชเฅ‹เคฐเฅเคŸ เคญเฅ€ เคถเคพเคฎเคฟเคฒ เคนเฅˆเฅค +**เคซเคผเคพเค‡เคฒ เคฎเฅˆเคจเฅ‡เคœเคฐ:** +SFTP เคธเฅ‡ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคฆเฅ‡เค–เฅ‡เค‚, เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเฅ‡เค‚, เค…เคชเคฒเฅ‹เคก เค”เคฐ เคกเคพเค‰เคจเคฒเฅ‹เคก เค•เคฐเฅ‡เค‚, เคจเคพเคฎ เคฌเคฆเคฒเฅ‡เค‚, เคนเคŸเคพเคเค เค”เคฐ เค–เคฟเคธเค•เคพเคเค, sudo เค•เฅ‡ เคธเคพเคฅ เคญเฅ€เฅค เค•เฅ‹เคก, เคคเคธเฅเคตเฅ€เคฐเฅ‡เค‚, เค‘เคกเคฟเคฏเฅ‹ เค”เคฐ เคตเฅ€เคกเคฟเคฏเฅ‹ เคฆเฅ‡เค–เฅ‡เค‚ เค”เคฐ เคฌเคฆเคฒเฅ‡เค‚เฅค เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคธเฅ€เคงเฅ‡ เคเค• เคธเคฐเฅเคตเคฐ เคธเฅ‡ เคฆเฅ‚เคธเคฐเฅ‡ เคชเคฐ เค•เฅ‰เคชเฅ€ เค•เคฐเฅ‡เค‚, เคธเคฌเคธเฅ‡ เคคเฅ‡เคœเคผ เคฐเคพเคธเฅเคคเคพ เค…เคชเคจเฅ‡ เค†เคช เคšเฅเคจเคพ เคœเคพเคคเคพ เคนเฅˆ เค”เคฐ เคŸเฅเคฐเคพเค‚เคธเคซเคผเคฐ เค•เฅ€ เคœเคพเคเคš เคญเฅ€ เคนเฅ‹เคคเฅ€ เคนเฅˆเฅค -**Docker เค”เคฐ Podman เคชเฅเคฐเคฌเค‚เคงเคจ:** -เค•เค‚เคŸเฅ‡เคจเคฐ เคถเฅเคฐเฅ‚, เคฌเค‚เคฆ, เคชเฅ‰เคœเคผ, เคนเคŸเคพเคเคเฅค เค•เค‚เคŸเฅ‡เคจเคฐ เคธเฅเคŸเฅˆเคŸเฅเคธ เคฆเฅ‡เค–เฅ‡เค‚เฅค docker exec เคŸเคฐเฅเคฎเคฟเคจเคฒ เค•เคพ เค‰เคชเคฏเฅ‹เค— เค•เคฐเค•เฅ‡ เค•เค‚เคŸเฅ‡เคจเคฐ เค•เฅ‹ เคจเคฟเคฏเค‚เคคเฅเคฐเคฟเคค เค•เคฐเฅ‡เค‚เฅค Docker เค”เคฐ Podman เคฆเฅ‹เคจเฅ‹เค‚ เค•เฅ‹ เค•เค‚เคŸเฅ‡เคจเคฐ เคฐเคจเคŸเคพเค‡เคฎ เค•เฅ‡ เคฐเฅ‚เคช เคฎเฅ‡เค‚ เคธเคชเฅ‹เคฐเฅเคŸ เค•เคฐเคคเคพ เคนเฅˆเฅค เค‡เคธเฅ‡ Portainer เคฏเคพ Dockge เค•เฅ€ เคœเค—เคน เคฒเฅ‡เคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคจเคนเฅ€เค‚ เคฌเคจเคพเคฏเคพ เค—เคฏเคพ เคฌเคฒเฅเค•เคฟ เค•เค‚เคŸเฅ‡เคจเคฐ เคฌเคจเคพเคจเฅ‡ เค•เฅ€ เคคเฅเคฒเคจเคพ เคฎเฅ‡เค‚ เค‰เคจเฅเคนเฅ‡เค‚ เคธเคฐเคฒเคคเคพ เคธเฅ‡ เคชเฅเคฐเคฌเค‚เคงเคฟเคค เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคฌเคจเคพเคฏเคพ เค—เคฏเคพ เคนเฅˆเฅค +**Docker เค”เคฐ Podman:** +เค•เค‚เคŸเฅ‡เคจเคฐ เคšเคพเคฒเฅ‚ เค•เคฐเฅ‡เค‚, เคฐเฅ‹เค•เฅ‡เค‚, เคฅเคพเคฎเฅ‡เค‚ เค”เคฐ เคนเคŸเคพเคเค, เค‰เคจเค•เฅ‡ เค†เคเค•เคกเคผเฅ‡ เคฆเฅ‡เค–เฅ‡เค‚, เค”เคฐ เค•เคฟเคธเฅ€ เคเค• เค•เฅ‡ เค…เค‚เคฆเคฐ เคถเฅ‡เคฒ เค–เฅ‹เคฒเฅ‡เค‚เฅค Docker เค”เคฐ Podman เคฆเฅ‹เคจเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ เคšเคฒเคคเคพ เคนเฅˆเฅค เคฏเคน Portainer เคฏเคพ Dockge เค•เฅ€ เคœเค—เคน เคฒเฅ‡เคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคจเคนเฅ€เค‚ เคนเฅˆ, เคธเคฟเคฐเฅเคซเคผ เค†เคชเค•เฅ‡ เคชเคนเคฒเฅ‡ เคธเฅ‡ เคฎเฅŒเคœเฅ‚เคฆ เค•เค‚เคŸเฅ‡เคจเคฐ เคธเค‚เคญเคพเคฒเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคนเฅˆเฅค -**SSH เคนเฅ‹เคธเฅเคŸ เคฎเฅˆเคจเฅ‡เคœเคฐ:** -เคŸเฅˆเค— เค”เคฐ เคซเคผเฅ‹เคฒเฅเคกเคฐ (เคซเคผเฅ‹เคฒเฅเคกเคฐ เค•เคธเฅเคŸเคฎเคพเค‡เคœเคผเฅ‡เคถเคจ เค”เคฐ เคจเฅ‡เคธเฅเคŸเฅ‡เคก เคซเคผเฅ‹เคฒเฅเคกเคฐ เคธเคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคธเคพเคฅ) เค•เฅ‡ เคธเคพเคฅ เค…เคชเคจเฅ‡ SSH เค•เคจเฅ‡เค•เฅเคถเคจ เคธเคนเฅ‡เคœเฅ‡เค‚, เคตเฅเคฏเคตเคธเฅเคฅเคฟเคค เค•เคฐเฅ‡เค‚ เค”เคฐ เคชเฅเคฐเคฌเค‚เคงเคฟเคค เค•เคฐเฅ‡เค‚, เค”เคฐ SSH เค•เฅเค‚เคœเคฟเคฏเฅ‹เค‚ เค•เฅ€ เคคเฅˆเคจเคพเคคเฅ€ เค•เฅ‹ เคธเฅเคตเคšเคพเคฒเคฟเคค เค•เคฐเคจเฅ‡ เค•เฅ€ เค•เฅเคทเคฎเคคเคพ เค•เฅ‡ เคธเคพเคฅ เคชเฅเคจ: เค‰เคชเคฏเฅ‹เค— เคฏเฅ‹เค—เฅเคฏ เคฒเฅ‰เค—เคฟเคจ เคœเคพเคจเค•เคพเคฐเฅ€ เค†เคธเคพเคจเฅ€ เคธเฅ‡ เคธเคนเฅ‡เคœเฅ‡เค‚เฅค +**เคนเฅ‹เคธเฅเคŸ เคฎเฅˆเคจเฅ‡เคœเคฐ:** +เคŸเฅˆเค— เค”เคฐ เคจเคพเคฎ เคต เคฐเค‚เค— เคตเคพเคฒเฅ‡ เคจเฅ‡เคธเฅเคŸเฅ‡เคก เคซเคผเฅ‹เคฒเฅเคกเคฐ เคธเฅ‡ เคนเฅ‹เคธเฅเคŸ เคธเคนเฅ‡เคœเฅ‡เค‚ เค”เคฐ เคตเฅเคฏเคตเคธเฅเคฅเคฟเคค เค•เคฐเฅ‡เค‚เฅค เคธเคนเฅ‡เคœเฅ‡ เค—เค เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เค•เคˆ เคนเฅ‹เคธเฅเคŸ เคชเคฐ เคฆเฅ‹เคฌเคพเคฐเคพ เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เค•เคฐเฅ‡เค‚, SSH เค•เฅเค‚เคœเคฟเคฏเคพเค เค…เคชเคจเฅ‡ เค†เคช เคญเฅ‡เคœเฅ‡เค‚, เคนเฅ‹เคธเฅเคŸ เค•เฅ‹ เค•เคฟเคธเฅ€ เคฎเฅเค–เฅเคฏ เคนเฅ‹เคธเฅเคŸ เค•เฅ‡ เคจเฅ€เคšเฅ‡ เคฐเค–เฅ‡เค‚, เคเค• เคธเคพเคฅ เค•เคˆ เคฎเฅ‡เค‚ เคฌเคฆเคฒเคพเคต เค•เคฐเฅ‡เค‚ เค”เคฐ เคจเคฟเคฐเฅเคฏเคพเคค เค•เคฐเฅ‡เค‚, เค”เคฐ เคœเคฟเคจ เค•เคจเฅ‡เค•เฅเคถเคจเฅ‹เค‚ เค•เฅ‹ เคธเคนเฅ‡เคœเคจเคพ เคจเคนเฅ€เค‚ เคšเคพเคนเคคเฅ‡ เค‰เคจเค•เฅ‡ เคฒเคฟเค เค•เฅเคตเคฟเค• เค•เคจเฅ‡เค•เฅเคŸ เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เค•เคฐเฅ‡เค‚เฅค @@ -111,83 +111,139 @@ Termix เคเค• เค“เคชเคจ-เคธเฅ‹เคฐเฅเคธ, เคนเคฎเฅ‡เคถเคพ เค•เฅ‡ เคฒเคฟเค เคฎเฅ **เคนเฅ‹เคธเฅเคŸ เคฎเฅ‡เคŸเฅเคฐเคฟเค•เฅเคธ:** -เค…เคงเคฟเค•เคพเค‚เคถ Linux เค†เคงเคพเคฐเคฟเคค เคธเคฐเฅเคตเคฐ เคชเคฐ CPU, เคฎเฅ‡เคฎเฅ‹เคฐเฅ€, เคกเคฟเคธเฅเค• เค‰เคชเคฏเฅ‹เค—, เคจเฅ‡เคŸเคตเคฐเฅเค•, เค…เคชเคŸเคพเค‡เคฎ, เคธเคฟเคธเฅเคŸเคฎ เคœเคพเคจเค•เคพเคฐเฅ€, เคซเคผเคพเคฏเคฐเคตเฅ‰เคฒ, เคชเฅ‹เคฐเฅเคŸ เคฎเฅ‰เคจเคฟเคŸเคฐ, เคฒเฅ‰เค— เคตเฅเคฏเฅ‚เค…เคฐ, เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ/เค…เคจเฅเคฎเคคเคฟเคฏเคพเค, เคธเคฐเฅเคŸเคฟเคซเคผเคฟเค•เฅ‡เคŸ เค”เคฐ เคญเฅ€ เคฌเคนเฅเคค เค•เฅเค› เคฆเฅ‡เค–เฅ‡เค‚เฅค เค‡เคธเคฎเฅ‡เค‚ เคŸเคพเค‡เคฎ-เคธเฅ€เคฐเฅ€เคœเคผ เคนเคฟเคธเฅเคŸเฅเคฐเฅ€ เค—เฅเคฐเคพเคซเคผ เค”เคฐ ntfy เคต webhook เคธเคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคธเคพเคฅ เคฅเฅเคฐเฅ‡เคถเฅ‹เคฒเฅเคก-เค†เคงเคพเคฐเคฟเคค เค…เคฒเคฐเฅเคŸ เคถเคพเคฎเคฟเคฒ เคนเฅˆเค‚เฅค +เคœเคผเฅเคฏเคพเคฆเคพเคคเคฐ Linux เคธเคฐเฅเคตเคฐ เคชเคฐ CPU, เคฎเฅ‡เคฎเฅ‹เคฐเฅ€, เคกเคฟเคธเฅเค•, เคจเฅ‡เคŸเคตเคฐเฅเค•, เคคเคพเคชเคฎเคพเคจ, เค…เคชเคŸเคพเค‡เคฎ, เคชเฅเคฐเฅ‹เคธเฅ‡เคธ, เคชเฅ‹เคฐเฅเคŸ, เคฒเฅ‰เค—เคฟเคจ เค”เคฐ เคธเคฟเคธเฅเคŸเคฎ เคœเคพเคจเค•เคพเคฐเฅ€, เคชเฅเคฐเคพเคจเฅ‡ เค†เคเค•เคกเคผเฅ‹เค‚ เค•เฅ‡ เค—เฅเคฐเคพเคซเคผ เค•เฅ‡ เคธเคพเคฅเฅค เคฎเฅˆเคจเฅ‡เคœเคฐ เค•เคพเคฐเฅเคก เคธเฅ‡ เค†เคช เคธเคฐเฅเคตเคฟเคธ, cron เค•เคพเคฐเฅเคฏ, เคชเฅˆเค•เฅ‡เคœ, เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ, เคซเคผเคพเคฏเคฐเคตเฅ‰เคฒ เคจเคฟเคฏเคฎ, WireGuard, Tailscale, SSL เคชเฅเคฐเคฎเคพเคฃเคชเคคเฅเคฐ, เคฒเฅ‰เค— เค”เคฐ เคนเฅ‡เคฒเฅเคฅ เคšเฅ‡เค• Termix เค›เฅ‹เคกเคผเฅ‡ เคฌเคฟเคจเคพ เคธเค‚เคญเคพเคฒ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค -**เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เคชเฅเคฐเคฎเคพเคฃเฅ€เค•เคฐเคฃ:** -เคตเฅเคฏเคตเคธเฅเคฅเคพเคชเค• เคจเคฟเคฏเค‚เคคเฅเคฐเคฃ (เค…เคจเฅเคฏ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพเค“เค‚ เค•เฅ€ เคœเคพเคจเค•เคพเคฐเฅ€ เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐ เคธเค•เคคเฅ‡ เคนเฅˆเค‚) เค”เคฐ OIDC/LDAP/SSO (เคเค•เฅเคธเฅ‡เคธ เค•เค‚เคŸเฅเคฐเฅ‹เคฒ เค•เฅ‡ เคธเคพเคฅ), 2FA (TOTP), เค”เคฐ เคชเคพเคธเค•เฅ€ (WebAuthn) เคธเคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคธเคพเคฅ เคธเฅเคฐเค•เฅเคทเคฟเคค เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เคชเฅเคฐเคฌเค‚เคงเคจเฅค เคธเคญเฅ€ เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคชเคฐ เคธเค•เฅเคฐเคฟเคฏ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เคธเคคเฅเคฐ เคฆเฅ‡เค–เฅ‡เค‚ เค”เคฐ เค…เคจเฅเคฎเคคเคฟเคฏเคพเค เคฐเคฆเฅเคฆ เค•เคฐเฅ‡เค‚เฅค เค…เคชเคจเฅ‡ OIDC/เคธเฅเคฅเคพเคจเฅ€เคฏ เค–เคพเคคเฅ‹เค‚ เค•เฅ‹ เคเค• เคธเคพเคฅ เคœเฅ‹เคกเคผเฅ‡เค‚เฅค เคธเคญเฅ€ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพเค“เค‚ เค•เฅ€ เค•เคพเคฐเฅเคฐเคตเคพเค‡เคฏเฅ‹เค‚ เค•เคพ เค‘เคกเคฟเคŸ เคฒเฅ‰เค— เคฆเฅ‡เค–เฅ‡เค‚เฅค +**เค‘เคŸเฅ‹เคฎเฅ‡เคถเคจ:** +เคชเคนเคฒเฅ‡ เคเค• เคŸเฅเคฐเคฟเค—เคฐ เคšเฅเคจเฅ‡เค‚, เคซเคฟเคฐ เคฌเคคเคพเคเค เค•เคฟ เค•เฅเคฏเคพ เคนเฅ‹เคจเคพ เคšเคพเคนเคฟเคเฅค เคŸเฅเคฐเคฟเค—เคฐ เคฎเฅ‡เค‚ เค•เคฟเคธเฅ€ เคฎเฅ‡เคŸเฅเคฐเคฟเค• เค•เคพ เคคเคฏ เคธเฅ€เคฎเคพ เคชเคพเคฐ เค•เคฐเคจเคพ, เคนเฅ‹เคธเฅเคŸ เค•เคพ เคฌเค‚เคฆ เคนเฅ‹เคจเคพ เคฏเคพ เคตเคพเคชเคธ เค†เคจเคพ, เคนเฅ‡เคฒเฅเคฅ เคšเฅ‡เค• เค•เคพ เคฌเคฆเคฒเคจเคพ, เค•เฅ‹เคˆ เคคเคฏ เคธเคฎเคฏ, เค•เค‚เคŸเฅ‡เคจเคฐ เค•เฅ€ เค•เฅ‹เคˆ เค˜เคŸเคจเคพ, เคฏเคพ เค†เคจเฅ‡ เคตเคพเคฒเคพ webhook เคถเคพเคฎเคฟเคฒ เคนเฅˆเฅค เค•เคฆเคฎเฅ‹เค‚ เคฎเฅ‡เค‚ เค•เคฎเคพเค‚เคก เค”เคฐ เคธเฅเคจเคฟเคชเฅ‡เคŸ เคšเคฒเคพเคจเคพ, เค•เค‚เคŸเฅ‡เคจเคฐ เค”เคฐ เคŸเคจเคฒ เคธเค‚เคญเคพเคฒเคจเคพ, เคฎเคถเฅ€เคจ เคœเค—เคพเคจเคพ, เค•เฅ‹เคˆ URL เคฌเฅเคฒเคพเคจเคพ, เค‡เค‚เคคเคœเคผเคพเคฐ เค•เคฐเคจเคพ, เคถเคฐเฅเคค เค•เฅ‡ เคนเคฟเคธเคพเคฌ เคธเฅ‡ เคฐเคพเคธเฅเคคเคพ เคฌเคฆเคฒเคจเคพ, เคฆเฅ‚เคธเคฐเคพ เค‘เคŸเฅ‹เคฎเฅ‡เคถเคจ เคšเคฒเคพเคจเคพ, เค”เคฐ ntfy, Discord เคฏเคพ webhook เคธเฅ‡ เค†เคชเค•เฅ‹ เคฌเคคเคพเคจเคพ เคถเคพเคฎเคฟเคฒ เคนเฅˆเฅค เคŸเฅ‡เคธเฅเคŸ เคฐเคจ เคธเฅ‡ เค†เคช เคชเคนเคฒเฅ‡ เคธเฅเคฐเค•เฅเคทเคฟเคค เคคเคฐเฅ€เค•เฅ‡ เคธเฅ‡ เค†เคœเคผเคฎเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค -**Tailscale เคเค•เฅ€เค•เคฐเคฃ:** -เค…เคชเคจเฅ‡ Tailscale เคจเฅ‡เคŸเคตเคฐเฅเค• เค•เฅ‡ เคกเคฟเคตเคพเค‡เคธ เคธเฅ‚เคšเฅ€เคฌเคฆเฅเคง เค•เคฐเฅ‡เค‚ เคคเคพเค•เคฟ เค‰เคจเฅเคนเฅ‡เค‚ เคœเคฒเฅเคฆเฅ€ เคธเฅ‡ เคนเฅ‹เคธเฅเคŸ เค•เฅ‡ เคฐเฅ‚เคช เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเคพ เคœเคพ เคธเค•เฅ‡, เค”เคฐ Tailscale SSH เค•เฅ‹ เคชเฅเคฐเคฎเคพเคฃเฅ€เค•เคฐเคฃ เคตเคฟเคงเคฟ เค•เฅ‡ เคฐเฅ‚เคช เคฎเฅ‡เค‚ เค‰เคชเคฏเฅ‹เค— เค•เคฐเค•เฅ‡ เค•เคจเฅ‡เค•เฅเคŸ เค•เคฐเฅ‡เค‚, เคœเคฟเคธเคธเฅ‡ เค†เคชเค•เฅ‡ Tailscale ACL เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เคธเค‚เค—เฅเคฐเคนเฅ€เคค เค•เคฟเค เคฌเคฟเคจเคพ เคชเฅเคฐเคพเคงเคฟเค•เคฐเคฃ เคธเค‚เคญเคพเคฒ เคธเค•เฅ‡เค‚เฅค +**เคซเคผเฅเคฒเฅ€เคŸ:** +เคนเฅ‹เคธเฅเคŸ เคšเฅเคจเค•เคฐ เคฏเคพ เคŸเฅˆเค— เคจเคฟเคฏเคฎเฅ‹เค‚ เคธเฅ‡ เคเค• เคซเคผเฅเคฒเฅ€เคŸ เคฌเคจเคพเคเค, เคคเคพเค•เคฟ เคจเค เคนเฅ‹เคธเฅเคŸ เค…เคชเคจเฅ‡ เค†เคช เคœเฅเคกเคผ เคœเคพเคเคเฅค เคเค• เคนเฅ€ เค•เคฎเคพเค‚เคก เคธเคญเฅ€ เคนเฅ‹เคธเฅเคŸ เคชเคฐ เคเค• เคธเคพเคฅ เคšเคฒเคพเคเค, เคธเคฌ เคชเคฐ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคญเฅ‡เคœเฅ‡เค‚ เค”เคฐ เค‰เคจเคธเฅ‡ เคฒเคพเคเค, เคชเฅˆเค•เฅ‡เคœ เค‡เค‚เคธเฅเคŸเฅ‰เคฒ เค•เคฐเฅ‡เค‚, เค”เคฐ OS, เค•เคฐเฅเคจเฅ‡เคฒ, เค†เคฐเฅเค•เคฟเคŸเฅ‡เค•เฅเคšเคฐ เค”เคฐ เค…เคชเคŸเคพเค‡เคฎ เค•เฅ€ เคธเฅ‚เคšเฅ€ เค‡เค•เคŸเฅเค เคพ เค•เคฐเฅ‡เค‚เฅค -**RBAC/เคถเฅ‡เคฏเคฐเคฟเค‚เค—:** -เคญเฅ‚เคฎเคฟเค•เคพเคเค เคฌเคจเคพเคเค เค”เคฐ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพเค“เค‚/เคญเฅ‚เคฎเคฟเค•เคพเค“เค‚ เคฎเฅ‡เค‚ เคนเฅ‹เคธเฅเคŸ เคธเคพเคเคพ เค•เคฐเฅ‡เค‚เฅค เคธเคญเฅ€ เคชเฅเคฐเคฎเคพเคฃเฅ€เค•เคฐเคฃ เคชเฅเคฐเค•เคพเคฐเฅ‹เค‚ เค”เคฐ เคธเคญเฅ€ เคนเฅ‹เคธเฅเคŸ เคชเฅเคฐเฅ‹เคŸเฅ‹เค•เฅ‰เคฒ เค•เคพ เคธเคชเฅ‹เคฐเฅเคŸ เค•เคฐเคคเคพ เคนเฅˆเฅค +**AI เคธเคนเคพเคฏเค•:** +เคฏเคน เคตเฅˆเค•เคฒเฅเคชเคฟเค• เคนเฅˆ เค”เคฐ เคœเคฌ เคคเค• เค†เคช เค–เฅเคฆ เคšเคพเคฒเฅ‚ เคจ เค•เคฐเฅ‡เค‚, เคฌเค‚เคฆ เคฐเคนเคคเคพ เคนเฅˆเฅค OpenAI, Anthropic, Gemini, Ollama เคฏเคพ OpenAI เค•เฅ‡ เค…เคจเฅเคฐเฅ‚เคช เค•เฅ‹เคˆ เคญเฅ€ เคเค‚เคกเคชเฅ‰เค‡เค‚เคŸ เคœเฅ‹เคกเคผเฅ‡เค‚ เค”เคฐ เค…เคชเคจเฅ‡ เคธเฅ‡เคŸเค…เคช เค•เฅ‡ เคฌเคพเคฐเฅ‡ เคฎเฅ‡เค‚ เคชเฅ‚เค›เฅ‡เค‚เฅค เคฏเคน เคนเฅ‹เคธเฅเคŸ, เคซเคผเฅเคฒเฅ€เคŸ, เคธเฅเคจเคฟเคชเฅ‡เคŸ เค”เคฐ เค…เคฒเคฐเฅเคŸ เคชเคขเคผ เคธเค•เคคเคพ เคนเฅˆ, เค”เคฐ เคฌเคฆเคฒเคพเคต เค–เฅเคฆ เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฌเคœเคพเคฏ เค†เคชเค•เฅ€ เคฎเค‚เคœเคผเฅ‚เคฐเฅ€ เค•เฅ‡ เคฒเคฟเค เคธเฅเคเคพเคคเคพ เคนเฅˆเฅค เคฏเคน เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ, เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพเค“เค‚ เคฏเคพ เคธเฅ‡เคŸเคฟเค‚เค—เฅเคธ เคคเค• เค•เคญเฅ€ เคจเคนเฅ€เค‚ เคชเคนเฅเคเคš เคธเค•เคคเคพเฅค เคเคกเคฎเคฟเคจ เค‡เคธเฅ‡ เคชเฅ‚เคฐเฅ‡ เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เค•เฅ‡ เคฒเคฟเค เคฌเค‚เคฆ เคฐเค– เคธเค•เคคเฅ‡ เคนเฅˆเค‚, เค”เคฐ เค†เคช เค‡เคธเฅ‡ เคถเฅเคฐเฅเค†เคคเฅ€ เคธเฅ‡เคŸเค…เคช เคฎเฅ‡เค‚ เคนเฅ€ เค›เคฟเคชเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค -**เคธเฅ€เคฐเคฟเคฏเคฒ เค•เคจเฅ‡เค•เฅเคถเคจ:** -เคธเฅ€เคฐเคฟเคฏเคฒ เคกเคฟเคตเคพเค‡เคธ (เคฐเคพเค‰เคŸเคฐ, เคธเฅเคตเคฟเคš, เคฎเคพเค‡เค•เฅเคฐเฅ‹เค•เค‚เคŸเฅเคฐเฅ‹เคฒเคฐ เค†เคฆเคฟ) เคธเฅ‡ เคธเฅ€เคงเฅ‡ เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคฏเคพ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเคช เคธเฅ‡ เค•เคจเฅ‡เค•เฅเคŸ เค•เคฐเฅ‡เค‚เฅค เคฌเฅ‰เคก เคฐเฅ‡เคŸ, เคกเฅ‡เคŸเคพ เคฌเคฟเคŸเฅเคธ, เคธเฅเคŸเฅ‰เคช เคฌเคฟเคŸเฅเคธ เค”เคฐ เคชเฅˆเคฐเคฟเคŸเฅ€ เค•เฅ‰เคจเฅเคซเคผเคฟเค—เคฐ เค•เคฐเฅ‡เค‚เฅค เคธเคฎเคฐเฅเคฅเคฟเคค เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคฎเฅ‡เค‚ Web Serial API เคฏเคพ Electron เคเคช เคฎเฅ‡เค‚ เคจเฅ‡เคŸเคฟเคต เคฌเฅˆเค•เคเค‚เคก เค•เคพ เค‰เคชเคฏเฅ‹เค— เค•เคฐเคคเคพ เคนเฅˆเฅค +**เคฒเฅ‰เค—เคฟเคจ เค”เคฐ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ:** +เคฒเฅ‹เค•เคฒ เค–เคพเคคเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ OIDC, LDAP, GitHub เค”เคฐ Google เคธเฅ‡ เคฒเฅ‰เค—เคฟเคจ, เค”เคฐ เคฆเฅ‹ เคšเคฐเคฃเฅ‹เค‚ เคตเคพเคฒเคพ เคธเคคเฅเคฏเคพเคชเคจ (TOTP), เคชเคพเคธเค•เฅ€ (WebAuthn) เคคเคฅเคพ เคญเคฐเฅ‹เคธเฅ‡เคฎเค‚เคฆ เคกเคฟเคตเคพเค‡เคธเฅค เคเคกเคฎเคฟเคจ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพเค“เค‚ เค•เฅ‹ เคธเค‚เคญเคพเคฒ เคธเค•เคคเฅ‡ เคนเฅˆเค‚, OIDC เคธเคฎเฅ‚เคนเฅ‹เค‚ เค•เฅ‹ เคญเฅ‚เคฎเคฟเค•เคพเค“เค‚ เคธเฅ‡ เคœเฅ‹เคกเคผ เคธเค•เคคเฅ‡ เคนเฅˆเค‚, เคนเคฐ เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคชเคฐ เคšเคพเคฒเฅ‚ เคธเคคเฅเคฐ เคฆเฅ‡เค– เค”เคฐ เคฐเคฆเฅเคฆ เค•เคฐ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค เค…เคชเคจเฅ‡ เคฒเฅ‹เค•เคฒ เค”เคฐ OIDC เค–เคพเคคเฅ‡ เค†เคชเคธ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเฅ‡เค‚, เค”เคฐ เค‘เคกเคฟเคŸ เคฒเฅ‰เค— เคฎเฅ‡เค‚ เคฆเฅ‡เค–เฅ‡เค‚ เค•เคฟ เค•เคฟเคธเคจเฅ‡ เค•เฅเคฏเคพ เค•เคฟเคฏเคพเฅค +**เคญเฅ‚เคฎเคฟเค•เคพเคเค เค”เคฐ เคธเคพเคเคพเค•เคฐเคฃ:** +เคญเฅ‚เคฎเคฟเค•เคพเคเค เคฌเคจเคพเคเค เค”เคฐ เคนเฅ‹เคธเฅเคŸ เค•เฅ‹ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพเค“เค‚ เคฏเคพ เคญเฅ‚เคฎเคฟเค•เคพเค“เค‚ เค•เฅ‡ เคธเคพเคฅ เคšเคพเคฐ เคธเฅเคคเคฐเฅ‹เค‚ เคชเคฐ เคธเคพเคเคพ เค•เคฐเฅ‡เค‚: เค•เคจเฅ‡เค•เฅเคŸ, เคฆเฅ‡เค–เคจเคพ, เคฌเคฆเคฒเคจเคพ เค”เคฐ เคชเฅเคฐเคฌเค‚เคงเคจเฅค เคฏเคน เคนเคฐ เคคเคฐเคน เค•เฅ‡ เคชเฅเคฐเคฎเคพเคฃเฅ€เค•เคฐเคฃ เค”เคฐ เคนเคฐ เคชเฅเคฐเฅ‹เคŸเฅ‹เค•เฅ‰เคฒ เค•เฅ‡ เคธเคพเคฅ เคšเคฒเคคเคพ เคนเฅˆ, เค”เคฐ เคธเคพเคเคพ เคนเฅ‹เคธเฅเคŸ เค•เฅ‡ เคฒเคฟเค เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เคนเฅ‹เคจเฅ‡ เคตเคพเคฒเฅ‡ เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เค†เคช เคฌเคฆเคฒ เคญเฅ€ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค + + + + + + **เค…เคฒเคฐเฅเคŸ:** -เคนเฅ‹เคธเฅเคŸ เคฎเฅ‡เคŸเฅเคฐเคฟเค•เฅเคธ (CPU, เคฎเฅ‡เคฎเฅ‹เคฐเฅ€, เคกเคฟเคธเฅเค• เค†เคฆเคฟ) เคชเคฐ เคฅเฅเคฐเฅ‡เคถเฅ‹เคฒเฅเคก-เค†เคงเคพเคฐเคฟเคค เค…เคฒเคฐเฅเคŸ เคจเคฟเคฏเคฎ เคธเฅ‡เคŸ เค•เคฐเฅ‡เค‚ เค”เคฐ เคœเคฌ เคตเฅ‡ เคŸเฅเคฐเคฟเค—เคฐ เคนเฅ‹เค‚ เคคเฅ‹ ntfy เคฏเคพ webhooks เค•เฅ‡ เคฎเคพเคงเฅเคฏเคฎ เคธเฅ‡ เคธเฅ‚เคšเคจเคพ เคชเคพเคเคเฅค เค‡เคคเคฟเคนเคพเคธ เคฒเฅ‰เค— เคฎเฅ‡เค‚ เคธเค•เฅเคฐเคฟเคฏ เค”เคฐ เคนเคฒ เค•เคฟเค เค—เค เค…เคฒเคฐเฅเคŸ เคฆเฅ‡เค–เฅ‡เค‚เฅค +CPU, เคฎเฅ‡เคฎเฅ‹เคฐเฅ€ เค”เคฐ เคกเคฟเคธเฅเค• เคœเฅˆเคธเฅ€ เคนเฅ‹เคธเฅเคŸ เคฎเฅ‡เคŸเฅเคฐเคฟเค•เฅเคธ เคชเคฐ เคจเคฟเคฏเคฎ เคฒเค—เคพเคเค, เค”เคฐ เค‰เคจเค•เฅ‡ เคšเคฒเคจเฅ‡ เคชเคฐ ntfy, Discord เคฏเคพ webhook เคธเฅ‡ เคธเฅ‚เคšเคจเคพ เคชเคพเคเคเฅค เคšเคฒ เคฐเคนเฅ‡ เค”เคฐ เค เฅ€เค• เคนเฅ‹ เคšเฅเค•เฅ‡ เค…เคฒเคฐเฅเคŸ เค‡เคคเคฟเคนเคพเคธ เคฎเฅ‡เค‚ เคฆเฅ‡เค–เฅ‡เค‚, เค”เคฐ เคœเฅ‹ เค†เคชเค•เฅ‡ เค•เคพเคฎ เค•เฅ‡ เคจเคนเฅ€เค‚ เค‰เคจเฅเคนเฅ‡เค‚ เคนเคŸเคพ เคฆเฅ‡เค‚เฅค - - **เคนเฅ‹เคฎเคชเฅ‡เคœ:** -เคกเฅเคฐเฅˆเค—-เคเค‚เคก-เคกเฅเคฐเฅ‰เคช เคตเคฟเคœเฅ‡เคŸ เค—เฅเคฐเคฟเคก เค•เฅ‡ เคธเคพเคฅ เคชเฅ‚เคฐเฅ€ เคคเคฐเคน เคธเฅ‡ เค•เคธเฅเคŸเคฎเคพเค‡เคœเคผ เค•เคฐเคจเฅ‡ เคฏเฅ‹เค—เฅเคฏ เคนเฅ‹เคฎเคชเฅ‡เคœเฅค เคนเฅ‹เคธเฅเคŸ เคธเฅเคŸเฅ‡เคŸเคธ, เคธเคฐเฅเคตเคฟเคธ เคฒเคฟเค‚เค•, เค˜เคกเคผเคฟเคฏเคพเค, เคจเฅ‹เคŸเฅเคธ, RSS เคซเคผเฅ€เคก, เคฎเฅŒเคธเคฎ, Docker เค•เค‚เคŸเฅ‡เคจเคฐ, เคนเฅ‹เคธเฅเคŸ เคฎเฅ‡เคŸเฅเคฐเคฟเค•เฅเคธ เคšเคพเคฐเฅเคŸ, เคเคฎเฅเคฌเฅ‡เคกเฅ‡เคก เคŸเคฐเฅเคฎเคฟเคจเคฒ, iframes เค”เคฐ เค…เคจเฅเคฏ เค•เฅ‡ เคฒเคฟเค เคตเคฟเคœเฅ‡เคŸ เคœเฅ‹เคกเคผเฅ‡เค‚เฅค - - - - -**เคกเฅ‡เคŸเคพเคฌเฅ‡เคธ เคเคจเฅเค•เฅเคฐเคฟเคชเฅเคถเคจ:** -เคฌเฅˆเค•เคเค‚เคก เคเคจเฅเค•เฅเคฐเคฟเคชเฅเคŸเฅ‡เคก SQLite เคกเฅ‡เคŸเคพเคฌเฅ‡เคธ เคซเคผเคพเค‡เคฒเฅ‹เค‚ เค•เฅ‡ เคฐเฅ‚เคช เคฎเฅ‡เค‚ เคธเค‚เค—เฅเคฐเคนเฅ€เคคเฅค เค…เคงเคฟเค• เคœเคพเคจเค•เคพเคฐเฅ€ เค•เฅ‡ เคฒเคฟเค [เคกเฅ‰เค•เฅเคธ](https://docs.termix.site/security) เคฆเฅ‡เค–เฅ‡เค‚เฅค +เค–เฅ€เค‚เคšเค•เคฐ เค›เฅ‹เคกเคผเคจเฅ‡ เคตเคพเคฒเคพ เคตเคฟเคœเฅ‡เคŸ เค—เฅเคฐเคฟเคก เคœเคฟเคธเฅ‡ เค†เคช เค–เฅเคฆ เคฌเคจเคพเคคเฅ‡ เคนเฅˆเค‚เฅค เคนเฅ‹เคธเฅเคŸ เค•เฅ€ เคธเฅเคฅเคฟเคคเคฟ, เคชเคฟเค‚เค—, เคธเคฐเฅเคตเคฟเคธ เคฒเคฟเค‚เค•, เคฌเฅเค•เคฎเคพเคฐเฅเค•, เค–เฅ‹เคœ, เค˜เคกเคผเคฟเคฏเคพเค, เค•เฅˆเคฒเฅ‡เค‚เคกเคฐ, เค‰เคฒเคŸเฅ€ เค—เคฟเคจเคคเฅ€, เคจเฅ‹เคŸเฅเคธ, RSS, เคฎเฅŒเคธเคฎ, เคคเคธเฅเคตเฅ€เคฐเฅ‡เค‚, iframe, Docker, เคŸเคจเคฒ, เคฎเฅ‡เคŸเฅเคฐเคฟเค•เฅเคธ เค•เฅ‡ เค—เฅเคฐเคพเคซเคผ, เค…เคชเคจเฅ‡ API เค”เคฐ เคฏเคนเคพเค เคคเค• เค•เคฟ เคšเคพเคฒเฅ‚ เคŸเคฐเฅเคฎเคฟเคจเคฒ เคคเค• เค•เฅ‡ เคตเคฟเคœเฅ‡เคŸ เคฎเฅŒเคœเฅ‚เคฆ เคนเฅˆเค‚เฅค -**เคจเฅ‡เคŸเคตเคฐเฅเค• เค—เฅเคฐเคพเคซเคผ:** -เคธเฅเคฅเคฟเคคเคฟ เคธเคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคธเคพเคฅ เค…เคชเคจเฅ‡ SSH เค•เคจเฅ‡เค•เฅเคถเคจ เค•เฅ‡ เค†เคงเคพเคฐ เคชเคฐ เค…เคชเคจเฅ‡ เคนเฅ‹เคฎเคฒเฅˆเคฌ เค•เฅ‹ เคตเคฟเคœเคผเฅเค…เคฒเคพเค‡เคœเคผ เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เค…เคชเคจเคพ เคกเฅˆเคถเคฌเฅ‹เคฐเฅเคก เค•เคธเฅเคŸเคฎเคพเค‡เคœเคผ เค•เคฐเฅ‡เค‚เฅค +**เคธเฅเคจเคฟเคชเฅ‡เคŸ เค”เคฐ เค‰เคชเค•เคฐเคฃ:** +เคœเฅ‹ เค•เคฎเคพเค‚เคก เค†เคช เคฌเคพเคฐ-เคฌเคพเคฐ เคšเคฒเคพเคคเฅ‡ เคนเฅˆเค‚ เค‰เคจเฅเคนเฅ‡เค‚ เคธเคนเฅ‡เคœเฅ‡เค‚ เค”เคฐ เคเค• เค•เฅเคฒเคฟเค• เคฎเฅ‡เค‚ เคšเคฒเคพเคเค, เคนเฅ‹เคธเฅเคŸ เค”เคฐ เค…เคชเคจเฅ‡ เค‡เคจเคชเฅเคŸ เค•เฅ‡ เคฒเคฟเค เคตเฅ‡เคฐเคฟเคเคฌเคฒ เค•เฅ‡ เคธเคพเคฅเฅค เคเค• เคนเฅ€ เค•เคฎเคพเค‚เคก เคธเคญเฅ€ เค–เฅเคฒเฅ‡ เคŸเคฐเฅเคฎเคฟเคจเคฒเฅ‹เค‚ เคชเคฐ เคšเคฒเคพเคเค, เค”เคฐ เค…เคชเคจเฅ‡ เค•เคฎเคพเค‚เคก เค‡เคคเคฟเคนเคพเคธ เคฎเฅ‡เค‚ เค‘เคŸเฅ‹-เค•เค‚เคชเฅเคฒเฅ€เคŸ เค•เฅ‡ เคธเคพเคฅ เค–เฅ‹เคœเฅ‡เค‚เฅค -**SSH เคŸเฅ‚เคฒเฅเคธ:** -เคเค• เค•เฅเคฒเคฟเค• เคธเฅ‡ เคจเคฟเคทเฅเคชเคพเคฆเคฟเคค เคนเฅ‹เคจเฅ‡ เคตเคพเคฒเฅ‡ เคชเฅเคจ: เค‰เคชเคฏเฅ‹เค— เคฏเฅ‹เค—เฅเคฏ เค•เคฎเคพเค‚เคก เคธเฅเคจเคฟเคชเฅ‡เคŸ เคฌเคจเคพเคเคเฅค เคเค• เคธเคพเคฅ เค•เคˆ เค–เฅเคฒเฅ‡ เคŸเคฐเฅเคฎเคฟเคจเคฒเฅ‹เค‚ เคฎเฅ‡เค‚ เคเค• เค•เคฎเคพเค‚เคก เคšเคฒเคพเคเคเฅค +**เคธเคคเฅเคฐ เคธเคพเคเคพ เค•เคฐเคจเคพ:** +เคšเคพเคฒเฅ‚ เคŸเคฐเฅเคฎเคฟเคจเคฒ, RDP, VNC เคฏเคพ Telnet เคธเคคเฅเคฐ เคฒเคพเค‡เคต เคธเคพเคเคพ เค•เคฐเฅ‡เค‚เฅค เคเคธเคพ เคฒเคฟเค‚เค• เคญเฅ‡เคœเฅ‡เค‚ เคœเคฟเคธเคธเฅ‡ เค•เฅ‹เคˆ เคญเฅ€ เคฌเคฟเคจเคพ เค–เคพเคคเฅ‡ เค•เฅ‡ เคœเฅเคกเคผ เคธเค•เฅ‡, เคฏเคพ เค•เคฟเคธเฅ€ เค–เคพเคธ Termix เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เค•เฅ‡ เคธเคพเคฅ เคธเคพเคเคพ เค•เคฐเฅ‡เค‚, เคธเคฟเคฐเฅเคซเคผ เคฆเฅ‡เค–เคจเฅ‡ เคฏเคพ เคฒเคฟเค–เคจเฅ‡ เค•เฅ€ เค…เคจเฅเคฎเคคเคฟ เค•เฅ‡ เคธเคพเคฅเฅค เคธเคพเคเคพเค•เคฐเคฃ เค…เคชเคจเฅ‡ เค†เคช เค–เคคเฅเคฎ เคนเฅ‹ เคธเค•เคคเคพ เคนเฅˆ เคฏเคพ เค•เคญเฅ€ เคญเฅ€ เคฐเคฆเฅเคฆ เค•เคฟเคฏเคพ เคœเคพ เคธเค•เคคเคพ เคนเฅˆ, เค”เคฐ เค‡เคธเฅ‡ เคชเฅ‚เคฐเฅ€ เคคเคฐเคน เคฏเคพ เคนเคฐ เคนเฅ‹เคธเฅเคŸ เค•เฅ‡ เคฒเคฟเค เค…เคฒเค— เคธเฅ‡ เคฌเค‚เคฆ เค•เคฟเคฏเคพ เคœเคพ เคธเค•เคคเคพ เคนเฅˆเฅค -**เคชเคฐเคธเคฟเคธเฅเคŸเฅ‡เค‚เคŸ เคŸเฅˆเคฌ:** -เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เคชเฅเคฐเฅ‹เคซเคผเคพเค‡เคฒ เคฎเฅ‡เค‚ เคธเค•เฅเคทเคฎ เคนเฅ‹เคจเฅ‡ เคชเคฐ SSH เคธเฅ‡เคถเคจ เค”เคฐ เคŸเฅˆเคฌ เคกเคฟเคตเคพเค‡เคธ/เคฐเฅ€เคซเฅเคฐเฅ‡เคถ เค•เฅ‡ เคชเคพเคฐ เค–เฅเคฒเฅ‡ เคฐเคนเคคเฅ‡ เคนเฅˆเค‚เฅค +**เคธเคคเฅเคฐ เคฐเคฟเค•เฅ‰เคฐเฅเคกเคฟเค‚เค— เค”เคฐ เคฒเฅ‰เค—:** +เคŸเคฐเฅเคฎเคฟเคจเคฒ, RDP เค”เคฐ VNC เคธเคคเฅเคฐ เคฐเคฟเค•เฅ‰เคฐเฅเคก เค•เคฐเฅ‡เค‚ เค”เคฐ เคฌเคพเคฆ เคฎเฅ‡เค‚ เคฆเฅ‡เค–เฅ‡เค‚เฅค เคธเคคเฅเคฐ เค•เฅ‡ เคธเคพเคฆเฅ‡ เคŸเฅ‡เค•เฅเคธเฅเคŸ เคฒเฅ‰เค— เคกเคพเค‰เคจเคฒเฅ‹เคก เค•เคฐเฅ‡เค‚, เค”เคฐ เค•เคจเฅ‡เค•เฅเคถเคจ เคฒเฅ‰เค— เคฆเฅ‡เค–เค•เคฐ เคœเคพเคจเฅ‡เค‚ เค•เคฟ เคœเฅเคกเคผเคคเฅ‡ เคธเคฎเคฏ เค…เคธเคฒ เคฎเฅ‡เค‚ เค•เฅเคฏเคพ เคนเฅเค†เฅค + + + + +**เคธเฅ€เคฐเคฟเคฏเคฒ เค•เคจเฅ‡เค•เฅเคถเคจ:** +เคฐเคพเค‰เคŸเคฐ, เคธเฅเคตเคฟเคš เค”เคฐ เคฎเคพเค‡เค•เฅเคฐเฅ‹เค•เค‚เคŸเฅเคฐเฅ‹เคฒเคฐ เคœเฅˆเคธเฅ‡ เคธเฅ€เคฐเคฟเคฏเคฒ เค‰เคชเค•เคฐเคฃเฅ‹เค‚ เคธเฅ‡ เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคฏเคพ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเคช เคธเฅ‡ เคฌเคพเคค เค•เคฐเฅ‡เค‚เฅค เคฌเฅ‰เคก เคฐเฅ‡เคŸ, เคกเฅ‡เคŸเคพ เคฌเคฟเคŸ, เคธเฅเคŸเฅ‰เคช เคฌเคฟเคŸ เค”เคฐ เคชเฅˆเคฐเคฟเคŸเฅ€ เคธเฅ‡เคŸ เค•เคฐเฅ‡เค‚เฅค เคธเคนเคฏเฅ‹เค—เฅ€ เคฌเฅเคฐเคพเค‰เคœเคผเคฐเฅ‹เค‚ เคฎเฅ‡เค‚ Web Serial API เค”เคฐ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเคช เคฎเฅ‡เค‚ เคฎเฅ‚เคฒ เคฌเฅˆเค•เคเค‚เคก เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เคนเฅ‹เคคเคพ เคนเฅˆเฅค + + + + + + +**Tailscale:** +เค…เคชเคจเฅ‡ tailnet เคธเฅ‡ เคกเคฟเคตเคพเค‡เคธ เคฒเคพเค•เคฐ เค•เฅเค› เคนเฅ€ เค•เฅเคฒเคฟเค• เคฎเฅ‡เค‚ เคนเฅ‹เคธเฅเคŸ เค•เฅ‡ เคฐเฅ‚เคช เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเฅ‡เค‚, เค”เคฐ Tailscale SSH เคธเฅ‡ เคœเฅเคกเคผเฅ‡เค‚ เคคเคพเค•เคฟ เคชเคนเฅเคเคš เค•เคพ เค•เคพเคฎ เค†เคชเค•เฅ‡ tailnet เค•เฅ‡ ACL เคธเค‚เคญเคพเคฒเฅ‡เค‚ เค”เคฐ เค•เฅ‹เคˆ เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เคธเคนเฅ‡เคœเคจเคพ เคจ เคชเคกเคผเฅ‡เฅค Headscale เค”เคฐ เค…เคชเคจเฅ‡ เคเค‚เคกเคชเฅ‰เค‡เค‚เคŸ เคญเฅ€ เคšเคฒเคคเฅ‡ เคนเฅˆเค‚เฅค + + + + +**Proxmox:** +เคนเฅ‹เคธเฅเคŸ เคธเฅ€เคงเฅ‡ เค•เคฟเคธเฅ€ Proxmox เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เคธเฅ‡ เคฒเคพเคเค, เค”เคฐ เคจเฅ‹เคก เคคเคฅเคพ เค—เฅ‡เคธเฅเคŸ เค•เฅ‡ เค†เคเค•เคกเคผเฅ‡, เคœเคฟเคจเคฎเฅ‡เค‚ CPU, เคฎเฅ‡เคฎเฅ‹เคฐเฅ€ เค”เคฐ เคธเฅเคŸเฅ‹เคฐเฅ‡เคœ เคถเคพเคฎเคฟเคฒ เคนเฅˆเค‚, เค…เคฒเค— เคŸเฅˆเคฌ เคฎเฅ‡เค‚ เคฆเฅ‡เค–เฅ‡เค‚เฅค + + + + + + +**เคตเคฐเฅเค•เคธเฅเคชเฅ‡เคธ เค”เคฐ เคŸเฅˆเคฌ:** +เคŸเฅˆเคฌ เค•เคพ เคเค• เคธเฅ‡เคŸ เค‰เคจเค•เฅ‡ เคธเฅเคชเฅเคฒเคฟเคŸ เคฒเฅ‡เค†เค‰เคŸ เค•เฅ‡ เคธเคพเคฅ เคธเคนเฅ‡เคœเฅ‡เค‚ เค”เคฐ เคชเฅ‚เคฐเคพ เค•เคพ เคชเฅ‚เคฐเคพ เคเค• เค•เฅเคฒเคฟเค• เคฎเฅ‡เค‚ เคซเคฟเคฐ เค–เฅ‹เคฒเฅ‡เค‚เฅค Termix เค†เคชเค•เคพ เคชเคฟเค›เคฒเคพ เคธเคคเฅเคฐ เคญเฅ€ เคฏเคพเคฆ เคฐเค–เคคเคพ เคนเฅˆ, เค‡เคธเคฒเคฟเค เคชเฅ‡เคœ เคฐเฅ€เคซเคผเฅเคฐเฅ‡เคถ เค•เคฐเคจเฅ‡ เคชเคฐ เค”เคฐ เคฆเฅ‚เคธเคฐเฅ‡ เคกเคฟเคตเคพเค‡เคธ เคชเคฐ เคญเฅ€ เค†เคชเค•เฅ‡ เคŸเฅˆเคฌ เคฒเฅŒเคŸ เค†เคคเฅ‡ เคนเฅˆเค‚เฅค + + + + +**เคจเคฟเคฐเฅเคฆเฅ‡เคถเคฟเคค เคธเฅ‡เคŸเค…เคช:** +เคเค• เค›เฅ‹เคŸเคพ เคธเคพ เคธเฅ‡เคŸเค…เคช เค†เคชเค•เฅ‹ เค‡เค‚เคŸเคฐเคซเคผเฅ‡เคธ เคชเฅเคฐเฅ€เคธเฅ‡เคŸ, เคฅเฅ€เคฎ, เคฎเคจเคšเคพเคนเฅ€ เคธเฅเคตเคฟเคงเคพเคเค เค”เคฐ เคชเคนเคฒเคพ เคนเฅ‹เคธเฅเคŸ เคšเฅเคจเคจเฅ‡ เคฎเฅ‡เค‚ เคฎเคฆเคฆ เค•เคฐเคคเคพ เคนเฅˆเฅค เคธเคฐเคฒ เคฎเฅ‹เคก เคตเคน เคธเคฌ เค›เคฟเคชเคพ เคฆเฅ‡เคคเคพ เคนเฅˆ เคœเฅ‹ เค†เคช เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เคจเคนเฅ€เค‚ เค•เคฐเคคเฅ‡, เค”เคฐ เค†เคช เคธเฅ‡เคŸเค…เคช เคฆเฅ‹เคฌเคพเคฐเคพ เคšเคฒเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚ เคฏเคพ เคชเฅเคฐเฅ€เคธเฅ‡เคŸ เค•เคญเฅ€ เคญเฅ€ เคฌเคฆเคฒ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค + + + + + + +**เคธเฅเคตเคคเค‚เคคเฅเคฐ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเคช เค”เคฐ เคธเคฟเค‚เค•:** +เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคเคช เค…เคชเคจเฅ‡ เคฒเฅ‹เค•เคฒ เคฌเฅˆเค•เคเค‚เคก เค”เคฐ เคกเฅ‡เคŸเคพเคฌเฅ‡เคธ เค•เฅ‡ เคธเคพเคฅ เคฌเคฟเคจเคพ เค•เคฟเคธเฅ€ เคธเคฐเฅเคตเคฐ เค•เฅ‡ เค…เค•เฅ‡เคฒเฅ‡ เคšเคฒเคคเคพ เคนเฅˆเฅค เค†เคช เค‡เคธเฅ‡ เค•เคฟเคธเฅ€ Termix เคธเคฐเฅเคตเคฐ เคธเฅ‡ เคœเฅ‹เคกเคผเค•เคฐ เคนเฅ‹เคธเฅเคŸ, เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ, เคธเฅเคจเคฟเคชเฅ‡เคŸ เคตเค—เฅˆเคฐเคน เคฆเฅ‹เคจเฅ‹เค‚ เคคเคฐเคซเคผ เคธเคฟเค‚เค• เค•เคฐ เคธเค•เคคเฅ‡ เคนเฅˆเค‚, เค”เคฐ เคšเฅเคจ เคธเค•เคคเฅ‡ เคนเฅˆเค‚ เค•เคฟ เค•เคจเฅ‡เค•เฅเคถเคจ เค†เคชเค•เฅ€ เคฎเคถเฅ€เคจ เคธเฅ‡ เคถเฅเคฐเฅ‚ เคนเฅ‹เค‚ เคฏเคพ เคธเคฐเฅเคตเคฐ เค•เฅ‡ เคฐเคพเคธเฅเคคเฅ‡เฅค + + + + +**เค•เคฎเคพเค‚เคก เคฒเคพเค‡เคจ:** +เค†เคชเค•เฅ‡ เคถเฅ‡เคฒ เค”เคฐ เคธเฅเค•เฅเคฐเคฟเคชเฅเคŸ เค•เฅ‡ เคฒเคฟเค `termix` CLIเฅค เคŸเคฐเฅเคฎเคฟเคจเคฒ เค–เฅ‹เคฒเฅ‡เค‚, เค•เคฟเคธเฅ€ เคเค• เคนเฅ‹เคธเฅเคŸ เคฏเคพ เคชเฅ‚เคฐเฅ‡ เคซเคผเฅเคฒเฅ€เคŸ เคชเคฐ เค•เคฎเคพเค‚เคก เคšเคฒเคพเคเค, SFTP เคธเฅ‡ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคญเฅ‡เคœเฅ‡เค‚, เค”เคฐ เคนเฅ‹เคธเฅเคŸ, เคธเฅเคจเคฟเคชเฅ‡เคŸ เคต เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เคธเค‚เคญเคพเคฒเฅ‡เค‚เฅค `npm install -g @termix-cli/cli` เคธเฅ‡ เค‡เค‚เคธเฅเคŸเฅ‰เคฒ เค•เคฐเฅ‡เค‚ เคฏเคพ เค…เคฒเค— เคฌเคพเค‡เคจเคฐเฅ€ เคฒเฅ‡เค‚เฅค [CLI เคฆเคธเฅเคคเคพเคตเฅ‡เคœเคผ](https://docs.termix.site/cli) เคฆเฅ‡เค–เฅ‡เค‚เฅค + + + + + + +**เคธเฅเคฐเค•เฅเคทเคพ:** +เคชเคพเคธเคตเคฐเฅเคก, เค•เฅเค‚เคœเคฟเคฏเคพเค เค”เคฐ เคฌเคพเค•เฅ€ เค—เฅ‹เคชเคจเฅ€เคฏ เคœเคพเคจเค•เคพเคฐเฅ€ เคนเคฐ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เค•เฅ‡ เคฒเคฟเค เค…เคฒเค— เคธเฅ‡ เคเคจเฅเค•เฅเคฐเคฟเคชเฅเคŸ เคนเฅ‹เคคเฅ€ เคนเฅˆ, เค”เคฐ เคกเฅ‡เคŸเคพเคฌเฅ‡เคธ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคญเฅ€ เคกเคฟเคธเฅเค• เคชเคฐ เคเคจเฅเค•เฅเคฐเคฟเคชเฅเคŸ เค•เฅ€ เคœเคพ เคธเค•เคคเฅ€ เคนเฅˆเค‚เฅค เคฏเคน เค•เฅˆเคธเฅ‡ เค•เคพเคฎ เค•เคฐเคคเคพ เคนเฅˆ, เคฏเคน [เคฆเคธเฅเคคเคพเคตเฅ‡เคœเคผ](https://docs.termix.site/security) เคฎเฅ‡เค‚ เคฆเฅ‡เค–เฅ‡เค‚เฅค **เคญเคพเคทเคพเคเค:** -เคฒเค—เคญเค— 30 เคญเคพเคทเคพเค“เค‚ เค•เคพ เคฌเคฟเคฒเฅเคŸ-เค‡เคจ เคธเคชเฅ‹เคฐเฅเคŸ ([Crowdin](https://docs.termix.site/translations) เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเคฌเค‚เคงเคฟเคค)เฅค +เคฒเค—เคญเค— 30 เคญเคพเคทเคพเคเค เคชเคนเคฒเฅ‡ เคธเฅ‡ เคฎเฅŒเคœเฅ‚เคฆ เคนเฅˆเค‚, เคœเคฟเคจเฅเคนเฅ‡เค‚ [Crowdin](https://docs.termix.site/translations) เคธเฅ‡ เคธเค‚เคญเคพเคฒเคพ เคœเคพเคคเคพ เคนเฅˆเฅค @@ -196,20 +252,23 @@ Termix เคเค• เค“เคชเคจ-เคธเฅ‹เคฐเฅเคธ, เคนเคฎเฅ‡เคถเคพ เค•เฅ‡ เคฒเคฟเค เคฎเฅ
-เค…เคงเคฟเค• เคตเคฟเคถเฅ‡เคทเคคเคพเคเค +เค”เคฐ เคญเฅ€ เคตเคฟเคถเฅ‡เคทเคคเคพเคเค
-- **เคกเฅˆเคถเคฌเฅ‹เคฐเฅเคก** - เค…เคชเคจเฅ‡ เคกเฅˆเคถเคฌเฅ‹เคฐเฅเคก เคชเคฐ เคเค• เคจเคœเคผเคฐ เคฎเฅ‡เค‚ เคธเคฐเฅเคตเคฐ เค•เฅ€ เคœเคพเคจเค•เคพเคฐเฅ€ เคฆเฅ‡เค–เฅ‡เค‚ -- **API เค•เฅเค‚เคœเคฟเคฏเคพเค** - เค‘เคŸเฅ‹เคฎเฅ‡เคถเคจ/CI เค•เฅ‡ เคฒเคฟเค เค‰เคชเคฏเฅ‹เค— เคนเฅ‡เคคเฅ เคธเคฎเคพเคชเฅเคคเคฟ เคคเคฟเคฅเคฟเคฏเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ-เคธเฅเค•เฅ‹เคชเฅเคก API เค•เฅเค‚เคœเคฟเคฏเคพเค เคฌเคจเคพเคเค -- **เคกเฅ‡เคŸเคพ เคเค•เฅเคธเคชเฅ‹เคฐเฅเคŸ/เค‡เคฎเฅเคชเฅ‹เคฐเฅเคŸ** - SSH เคนเฅ‹เคธเฅเคŸ, เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เค”เคฐ เคซเคผเคพเค‡เคฒ เคฎเฅˆเคจเฅ‡เคœเคฐ เคกเฅ‡เคŸเคพ เคเค•เฅเคธเคชเฅ‹เคฐเฅเคŸ เค”เคฐ เค‡เคฎเฅเคชเฅ‹เคฐเฅเคŸ เค•เคฐเฅ‡เค‚ -- **เคธเฅเคตเคšเคพเคฒเคฟเคค SSL เคธเฅ‡เคŸเค…เคช** - HTTPS เคฐเฅ€เคกเคพเคฏเคฐเฅ‡เค•เฅเคŸ เค•เฅ‡ เคธเคพเคฅ เคฌเคฟเคฒเฅเคŸ-เค‡เคจ SSL เคธเคฐเฅเคŸเคฟเคซเคผเคฟเค•เฅ‡เคŸ เคœเคจเคฐเฅ‡เคถเคจ เค”เคฐ เคชเฅเคฐเคฌเค‚เคงเคจ -- **เค†เคงเฅเคจเคฟเค• UI** - React, Tailwind CSS, เค”เคฐ Shadcn เคธเฅ‡ เคฌเคจเคพ เคธเคพเคซเคผ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช/เคฎเฅ‹เคฌเคพเค‡เคฒ-เคซเคผเฅเคฐเฅ‡เค‚เคกเคฒเฅ€ เค‡เค‚เคŸเคฐเคซเคผเฅ‡เคธเฅค เคฒเคพเค‡เคŸ, เคกเคพเคฐเฅเค•, เคกเฅเคฐเฅˆเค•เฅเคฒเคพ เค†เคฆเคฟ เคธเคนเคฟเคค เค•เคˆ เค…เคฒเค—-เค…เคฒเค— UI เคฅเฅ€เคฎ เค•เฅ‡ เคฌเฅ€เคš เคšเฅเคจเฅ‡เค‚เฅค เค•เคฟเคธเฅ€ เคญเฅ€ เค•เคจเฅ‡เค•เฅเคถเคจ เค•เฅ‹ เคซเคผเฅเคฒ-เคธเฅเค•เฅเคฐเฅ€เคจ เคฎเฅ‡เค‚ เค–เฅ‹เคฒเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค URL เคฐเฅ‚เคŸ เค•เคพ เค‰เคชเคฏเฅ‹เค— เค•เคฐเฅ‡เค‚เฅค -- **เค•เคฎเคพเค‚เคก เค‡เคคเคฟเคนเคพเคธ** - เคชเคนเคฒเฅ‡ เคšเคฒเคพเค เค—เค SSH เค•เคฎเคพเค‚เคก เค•เคพ เค‘เคŸเฅ‹-เค•เคฎเฅเคชเฅเคฒเฅ€เคŸ เค”เคฐ เคฆเฅƒเคถเฅเคฏ -- **เค•เฅเคตเคฟเค• เค•เคจเฅ‡เค•เฅเคŸ** - เค•เคจเฅ‡เค•เฅเคถเคจ เคกเฅ‡เคŸเคพ เคธเคนเฅ‡เคœเฅ‡ เคฌเคฟเคจเคพ เคธเคฐเฅเคตเคฐ เคธเฅ‡ เค•เคจเฅ‡เค•เฅเคŸ เค•เคฐเฅ‡เค‚ -- **เค•เคฎเคพเค‚เคก เคชเฅˆเคฒเฅ‡เคŸ** - เค…เคชเคจเฅ‡ เค•เฅ€เคฌเฅ‹เคฐเฅเคก เคธเฅ‡ SSH เค•เคจเฅ‡เค•เฅเคถเคจ เคคเค• เคคเฅเคตเคฐเคฟเคค เคชเคนเฅเคเคš เค•เฅ‡ เคฒเคฟเค เคฌเคพเคเค Shift เค•เฅ‹ เคฆเฅ‹ เคฌเคพเคฐ เคŸเฅˆเคช เค•เคฐเฅ‡เค‚ -- **Proxmox เคเค•เฅ€เค•เคฐเคฃ** - เค…เคชเคจเฅ‡ Proxmox เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เคธเฅ‡ Termix เคฎเฅ‡เค‚ เคนเฅ‹เคธเฅเคŸ เคธเฅเคตเคšเคพเคฒเคฟเคค เคฐเฅ‚เคช เคธเฅ‡ เคœเฅ‹เคกเคผเฅ‡เค‚ -- **SSH เคธเฅเคตเคฟเคงเคพเค“เค‚ เคธเฅ‡ เคญเคฐเคชเฅ‚เคฐ** - เคœเคฎเฅเคช เคนเฅ‹เคธเฅเคŸ, Warpgate, TOTP เค†เคงเคพเคฐเคฟเคค เค•เคจเฅ‡เค•เฅเคถเคจ, SOCKS5, เคนเฅ‹เคธเฅเคŸ เค•เฅ€ เคตเฅ‡เคฐเคฟเคซเคผเคฟเค•เฅ‡เคถเคจ, เคชเคพเคธเคตเคฐเฅเคก เค‘เคŸเฅ‹เคซเคผเคฟเคฒ, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, เคชเฅ‹เคฐเฅเคŸ เคจเฅ‰เค•เคฟเค‚เค—, เคŸเคฐเฅเคฎเคฟเคจเคฒ เคฒเฅ‰เค—เคฟเค‚เค—, SSH เคเคœเฅ‡เค‚เคŸ เคซเคผเฅ‰เคฐเคตเคฐเฅเคกเคฟเค‚เค—, Bitwarden SSH เคเคœเฅ‡เค‚เคŸ, HashiCorp Vault SSH เคธเคฟเค—เฅเคจเคฟเค‚เค—, เค”เคฐ เค…เคจเฅเคฏ เค•เคพ เคธเคชเฅ‹เคฐเฅเคŸเฅค -- **Termix ID** - Termix เคฎเฅ‡เค‚ เคฌเคฟเคฒเฅเคŸ-เค‡เคจ sshid.io เค•เฅ‡ เคธเคฎเค•เค•เฅเคทเฅค เคเค• เคนเฅˆเค‚เคกเคฒ เค•เฅเคฒเฅ‡เคฎ เค•เคฐเฅ‡เค‚, เค…เคชเคจเฅ€ เคธเคพเคฐเฅเคตเคœเคจเคฟเค• SSH เค•เฅเค‚เคœเคฟเคฏเฅ‹เค‚ เค•เฅ‹ เคเค• เคฐเคฟเคœเคผเฅ‰เคฒเฅเคตเคฐ URL เคชเคฐ เคชเฅเคฐเค•เคพเคถเคฟเคค เค•เคฐเฅ‡เค‚, เค”เคฐ SSH เคธเคฐเฅเคŸเคฟเคซเคผเคฟเค•เฅ‡เคŸ เคœเคพเคฐเฅ€ เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคฌเคฟเคฒเฅเคŸ-เค‡เคจ CA เค•เคพ เค‰เคชเคฏเฅ‹เค— เค•เคฐเฅ‡เค‚เฅค +- **เคกเฅˆเคถเคฌเฅ‹เคฐเฅเคก** - เค†เคชเค•เฅ‡ เคธเคฐเฅเคตเคฐ เคเค• เคจเคœเคผเคฐ เคฎเฅ‡เค‚, เคเคธเฅ‡ เค•เคพเคฐเฅเคก เค•เฅ‡ เคธเคพเคฅ เคœเคฟเคจเฅเคนเฅ‡เค‚ เค†เคช เค–เฅเคฆ เคœเคฎเคพเคคเฅ‡ เคนเฅˆเค‚ +- **เคจเฅ‡เคŸเคตเคฐเฅเค• เค—เฅเคฐเคพเคซเคผ** - เค†เคชเค•เฅ‡ เคนเฅ‹เคธเฅเคŸ เคธเฅ‡ เคฌเคจเคพ เค†เคชเค•เคพ เคนเฅ‹เคฎเคฒเฅˆเคฌ เค•เคพ เคจเค•เฅเคถเคพ, เคฒเคพเค‡เคต เคธเฅเคฅเคฟเคคเคฟ เค•เฅ‡ เคธเคพเคฅ +- **tmux เคฎเฅ‰เคจเคฟเคŸเคฐ** - tmux เค•เฅ‡ เคธเคคเฅเคฐ, เคตเคฟเค‚เคกเฅ‹ เค”เคฐ เคชเฅˆเคจ เคฆเฅ‡เค–เฅ‡เค‚, เคเคฒเค• เค”เคฐ เค–เฅ‹เคœ เค•เฅ‡ เคธเคพเคฅ +- **API เค•เฅเค‚เคœเคฟเคฏเคพเค** - เคธเฅเค•เฅเคฐเคฟเคชเฅเคŸ เค”เคฐ CI เค•เฅ‡ เคฒเคฟเค, เคธเคฎเคพเคชเฅเคคเคฟ เคคเคฟเคฅเคฟ เคตเคพเคฒเฅ€ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ-เคตเคฟเคถเคฟเคทเฅเคŸ เค•เฅเค‚เคœเคฟเคฏเคพเค +- **เคจเคฟเคฐเฅเคฏเคพเคค เค”เคฐ เค†เคฏเคพเคค** - เคนเฅ‹เคธเฅเคŸ, เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เค”เคฐ เคซเคผเคพเค‡เคฒ เคฎเฅˆเคจเฅ‡เคœเคฐ เค•เคพ เคกเฅ‡เคŸเคพ เค…เค‚เคฆเคฐ-เคฌเคพเคนเคฐ เคฒเฅ‡ เคœเคพเคเค +- **เค…เคชเคจเฅ‡ เค†เคช SSL** - เคชเฅเคฐเคฎเคพเคฃเคชเคคเฅเคฐ เค†เคชเค•เฅ‡ เคฒเคฟเค เคฌเคจเคคเฅ‡ เค”เคฐ เคจเคตเฅ€เคจเฅ€เค•เฅƒเคค เคนเฅ‹เคคเฅ‡ เคนเฅˆเค‚, HTTPS เคฐเฅ€เคกเคพเคฏเคฐเฅ‡เค•เฅเคŸ เค•เฅ‡ เคธเคพเคฅ, เคฏเคพ เค…เคชเคจเฅ‡ เค–เฅเคฆ เค•เฅ‡ เคฒเค—เคพเคเค +- **เคกเฅ‡เคŸเคพเคฌเฅ‡เคธ** - เคกเคฟเคซเคผเฅ‰เคฒเฅเคŸ เคฐเฅ‚เคช เคธเฅ‡ SQLite, เคธเคพเคฅ เคฎเฅ‡เค‚ PostgreSQL เค”เคฐ MySQL เคญเฅ€ +- **เค†เคงเฅเคจเคฟเค• เค‡เค‚เคŸเคฐเคซเคผเฅ‡เคธ** - เคธเคพเคซเคผ เคธเฅเคฅเคฐเคพ React เค‡เค‚เคŸเคฐเคซเคผเฅ‡เคธ เคœเฅ‹ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เค”เคฐ เคฎเฅ‹เคฌเคพเค‡เคฒ เคฆเฅ‹เคจเฅ‹เค‚ เคชเคฐ เคšเคฒเคคเคพ เคนเฅˆ, เคฒเคพเค‡เคŸ, เคกเคพเคฐเฅเค• เค”เคฐ Dracula เคœเฅˆเคธเฅ€ เคฅเฅ€เคฎ เค•เฅ‡ เคธเคพเคฅเฅค เค•เฅ‹เคˆ เคญเฅ€ เค•เคจเฅ‡เค•เฅเคถเคจ URL เคธเฅ‡ เคชเฅ‚เคฐเฅ€ เคธเฅเค•เฅเคฐเฅ€เคจ เคฎเฅ‡เค‚ เค–เฅเคฒ เคธเค•เคคเคพ เคนเฅˆ +- **เค•เคฎเคพเค‚เคก เคชเฅˆเคฒเฅ‡เคŸ** - เคฌเคพเคˆเค‚ Shift เคฆเฅ‹ เคฌเคพเคฐ เคฆเคฌเคพเค•เคฐ เค•เฅ€เคฌเฅ‹เคฐเฅเคก เคธเฅ‡ เคธเฅ€เคงเฅ‡ เค•เคฟเคธเฅ€ เคนเฅ‹เคธเฅเคŸ เคชเคฐ เคœเคพเคเค +- **เค•เฅ€เคฌเฅ‹เคฐเฅเคก เคถเฅ‰เคฐเฅเคŸเค•เคŸ** - เคŸเฅˆเคฌ เคฌเคฆเคฒเคจเคพ, เคฌเค‚เคฆ เค•เคฐเคจเคพ เค”เคฐ เคฌเคนเฅเคค เค•เฅเค›, เคธเคฌ เคฆเฅ‹เคฌเคพเคฐเคพ เคคเคฏ เค•เคฟเค เคœเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚ +- **Wake-on-LAN** - เค•เคฟเคธเฅ€ เคฎเคถเฅ€เคจ เค•เฅ‹ Termix เคธเฅ‡ เคฏเคพ เค‘เคŸเฅ‹เคฎเฅ‡เคถเคจ เค•เฅ‡ เค•เคฟเคธเฅ€ เค•เคฆเคฎ เคธเฅ‡ เคœเค—เคพเคเค +- **เคญเคฐเฅ‹เคธเฅ‡เคฎเค‚เคฆ เคชเฅเคฐเฅ‰เค•เฅเคธเฅ€ เคธเฅ‡ เคฒเฅ‰เค—เคฟเคจ** - เคฐเคฟเคตเคฐเฅเคธ เคชเฅเคฐเฅ‰เค•เฅเคธเฅ€ เค•เฅ‹ เคฒเฅ‰เค—เคฟเคจ เคธเค‚เคญเคพเคฒเคจเฅ‡ เคฆเฅ‡เค‚ เค”เคฐ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เค•เฅ€ เคœเคพเคจเค•เคพเคฐเฅ€ เค†เค—เฅ‡ เคญเฅ‡เคœเคจเฅ‡ เคฆเฅ‡เค‚ +- **เคญเคฐเคชเฅ‚เคฐ SSH เคธเฅเคตเคฟเคงเคพเคเค** - เคœเค‚เคช เคนเฅ‹เคธเฅเคŸ, Warpgate, TOTP เคชเฅ‚เค›เคจเคพ, SOCKS5, เคนเฅ‹เคธเฅเคŸ เค•เฅเค‚เคœเฅ€ เค•เฅ€ เคœเคพเคเคš, เคชเคพเคธเคตเคฐเฅเคก เค…เคชเคจเฅ‡ เค†เคช เคญเคฐเคจเคพ, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, เคชเฅ‹เคฐเฅเคŸ เคจเฅ‰เค•เคฟเค‚เค—, เคŸเคฐเฅเคฎเคฟเคจเคฒ เคฒเฅ‰เค—, เคเคœเฅ‡เค‚เคŸ เคซเคผเฅ‰เคฐเคตเคฐเฅเคกเคฟเค‚เค—, Bitwarden SSH เคเคœเฅ‡เค‚เคŸ, HashiCorp Vault เคธเฅ‡ SSH เคนเคธเฅเคคเคพเค•เฅเคทเคฐ เค”เคฐ เคญเฅ€ เคฌเคนเฅเคค เค•เฅเค› +- **Termix ID** - sshid.io เคœเฅˆเคธเคพ เค…เคชเคจเคพ เคฌเคจเคพ เคนเฅเค† เค‡เค‚เคคเคœเคผเคพเคฎเฅค เคเค• เคจเคพเคฎ เคฒเฅ‡เค‚, เค…เคชเคจเฅ€ เคธเคพเคฐเฅเคตเคœเคจเคฟเค• เค•เฅเค‚เคœเคฟเคฏเคพเค เคเค• เคฐเคฟเคœเคผเฅ‰เคฒเฅเคตเคฐ URL เคชเคฐ เคฐเค–เฅ‡เค‚, เค”เคฐ เค…เค‚เคฆเคฐ เคฎเฅŒเคœเฅ‚เคฆ CA เคธเฅ‡ SSH เคชเฅเคฐเคฎเคพเคฃเคชเคคเฅเคฐ เคœเคพเคฐเฅ€ เค•เคฐเฅ‡เค‚
@@ -252,9 +311,9 @@ Termix เคเค• เค“เคชเคจ-เคธเฅ‹เคฐเฅเคธ, เคนเคฎเฅ‡เคถเคพ เค•เฅ‡ เคฒเคฟเค เคฎเฅ ## เค‡เค‚เคธเฅเคŸเฅ‰เคฒเฅ‡เคถเคจ -เคธเคญเฅ€ เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคชเคฐ เคชเฅ‚เคฐเฅเคฃ เค‡เค‚เคธเฅเคŸเฅ‰เคฒเฅ‡เคถเคจ เคจเคฟเคฐเฅเคฆเฅ‡เคถเฅ‹เค‚ เค•เฅ‡ เคฒเคฟเค Termix [เคกเฅ‰เค•เฅเคธ](https://docs.termix.site/install) เคชเคฐ เคœเคพเคเคเฅค +เคธเคญเฅ€ เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคชเคฐ เคชเฅ‚เคฐเฅ€ เค‡เค‚เคธเฅเคŸเฅ‰เคฒเฅ‡เคถเคจ เคœเคพเคจเค•เคพเคฐเฅ€ เค•เฅ‡ เคฒเคฟเค [Termix เคฆเคธเฅเคคเคพเคตเฅ‡เคœเคผ](https://docs.termix.site/install) เคฆเฅ‡เค–เฅ‡เค‚เฅค -เคจเคฎเฅ‚เคจเคพ Docker Compose เคซเคผเคพเค‡เคฒ (เคฏเคฆเคฟ เค†เคช เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เคธเฅเคตเคฟเคงเคพเค“เค‚ เค•เคพ เค‰เคชเคฏเฅ‹เค— เค•เคฐเคจเฅ‡ เค•เฅ€ เคฏเฅ‹เคœเคจเคพ เคจเคนเฅ€เค‚ เคฌเคจเคพ เคฐเคนเฅ‡ เคนเฅˆเค‚ เคคเฅ‹ เค†เคช `guacd` เค”เคฐ เคจเฅ‡เคŸเคตเคฐเฅเค• เค•เฅ‹ เคนเคŸเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚): +Docker Compose เคซเคผเคพเค‡เคฒ เค•เคพ เคจเคฎเฅ‚เคจเคพ (เค…เค—เคฐ เค†เคช เคฐเคฟเคฎเฅ‹เคŸ เคกเฅ‡เคธเฅเค•เคŸเฅ‰เคช เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เคจเคนเฅ€เค‚ เค•เคฐเคจเฅ‡ เคตเคพเคฒเฅ‡ เคคเฅ‹ `guacd` เค”เคฐ เคจเฅ‡เคŸเคตเคฐเฅเค• เคตเคพเคฒเคพ เคนเคฟเคธเฅเคธเคพ เคนเคŸเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚): ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### เค•เคฎเคพเค‚เคก เคฒเคพเค‡เคจ + +Termix เคฎเฅ‡เค‚ CLI เคญเฅ€ เคนเฅˆ, เคคเคพเค•เคฟ เค†เคช เคŸเคฐเฅเคฎเคฟเคจเคฒ เคธเฅ‡ เค…เคชเคจเฅ‡ เคธเคฐเฅเคตเคฐ เคธเค‚เคญเคพเคฒ เคธเค•เฅ‡เค‚ เค”เคฐ Termix เค•เฅ‹ เค…เคชเคจเฅ€ เคธเฅเค•เฅเคฐเคฟเคชเฅเคŸ เคฎเฅ‡เค‚ เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เค•เคฐ เคธเค•เฅ‡เค‚เฅค + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +เคฏเคน เคŸเคฐเฅเคฎเคฟเคจเคฒ เค–เฅ‹เคฒ เคธเค•เคคเคพ เคนเฅˆ, เคเค• เคนเฅ‹เคธเฅเคŸ เคฏเคพ เคชเฅ‚เคฐเฅ‡ เคซเคผเฅเคฒเฅ€เคŸ เคชเคฐ เค•เคฎเคพเค‚เคก เคšเคฒเคพ เคธเค•เคคเคพ เคนเฅˆ, SFTP เคธเฅ‡ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคฒเฅ‡ เคœเคพ เคธเค•เคคเคพ เคนเฅˆ, เค”เคฐ เคนเฅ‹เคธเฅเคŸ, เคธเฅเคจเคฟเคชเฅ‡เคŸ เคต เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เคธเค‚เคญเคพเคฒ เคธเค•เคคเคพ เคนเฅˆเฅค เคชเฅ‚เคฐเคพ เคฆเคธเฅเคคเคพเคตเฅ‡เคœเคผ [docs.termix.site/cli](https://docs.termix.site/cli) เคชเคฐ เคนเฅˆเฅค + +### เค•เฅเคฒเคพเค‰เคก เคนเฅ‹เคธเฅเคŸเคฟเค‚เค— + +เค†เคช Termix เคธเคฐเฅเคตเคฐ เค•เฅ‹ เค…เคชเคจเฅ‡ เคจเฅ‡เคŸเคตเคฐเฅเค• เค•เฅ‡ เคฌเคœเคพเคฏ เค•เคฟเคธเฅ€ VPS เคชเคฐ เคญเฅ€ เคšเคฒเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค เค…เค—เคฐ Termix เค‰เคธเฅ€ เคจเฅ‡เคŸเคตเคฐเฅเค• เคชเคฐ เคšเคฒ เคฐเคนเคพ เคนเฅˆ เคœเคฟเคธเฅ‡ เคตเคน เคธเค‚เคญเคพเคฒเคคเคพ เคนเฅˆ, เคคเฅ‹ เค—เคกเคผเคฌเคกเคผเฅ€ เคนเฅ‹เคจเฅ‡ เคชเคฐ เคตเคน เคญเฅ€ เคธเคพเคฅ เคนเฅ€ เคฌเค‚เคฆ เคนเฅ‹ เคœเคพเคเค—เคพ, เค เฅ€เค• เค‰เคธเฅ€ เคตเค•เฅเคค เคœเคฌ เค†เคชเค•เฅ‹ เค‰เคธเฅ‡ เค เฅ€เค• เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคšเคพเคนเคฟเคเฅค เคฌเคพเคนเคฐ เคšเคฒเคพเคจเฅ‡ เคชเคฐ เคตเคน เคนเคฎเฅ‡เคถเคพ เคชเคนเฅเคเคš เคฎเฅ‡เค‚ เคฐเคนเคคเคพ เคนเฅˆ, เคเค• เคธเฅเคฅเคฟเคฐ IP เคฆเฅ‡เคคเคพ เคนเฅˆ, เค”เคฐ เคฌเคฟเคจเคพ VPN เคฏเคพ เคชเฅ‹เคฐเฅเคŸ เคซเคผเฅ‰เคฐเคตเคฐเฅเคก เค•เฅ‡ เค•เคนเฅ€เค‚ เคธเฅ‡ เคญเฅ€ เคชเคนเฅเคเคš เคฎเคฟเคฒเคคเฅ€ เคนเฅˆเฅค + +[GINERNET](https://docs.termix.site/install/ginernet) Termix เค•เฅ‹ เคชเฅเคฐเคพเคฏเฅ‹เคœเคฟเคค เค•เคฐเคคเคพ เคนเฅˆ, เค”เคฐ เคฆเคธเฅเคคเคพเคตเฅ‡เคœเคผ เคฎเฅ‡เค‚ เค‰เคจเค•เฅ‡ VPS เคชเฅเคฒเฅ‡เคŸเคซเคผเฅ‰เคฐเฅเคฎ เคชเคฐ เคคเฅˆเคจเคพเคคเฅ€ เค•เฅ€ เค•เคฆเคฎ-เคฆเคฐ-เค•เคฆเคฎ เค—เคพเค‡เคก เคฎเฅŒเคœเฅ‚เคฆ เคนเฅˆเฅค + +
+ +## เคŸเฅ‡เคฒเฅ€เคฎเฅ‡เคŸเฅเคฐเฅ€ + +Termix เคฆเคฟเคจ เคฎเฅ‡เค‚ เคเค• เคฌเคพเคฐ เคเค• เค›เฅ‹เคŸเคพ เคธเคพ เค—เฅเคฎเคจเคพเคฎ เคธเค‚เค•เฅ‡เคค เคญเฅ‡เคœเคคเคพ เคนเฅˆ, เคคเคพเค•เคฟ เคฎเฅเคเฅ‡ เคชเคคเคพ เคšเคฒเฅ‡ เค•เคฟ เค•เคฟเคคเคจเฅ‡ เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เคšเคฒ เคฐเคนเฅ‡ เคนเฅˆเค‚ เค”เคฐ เค•เฅŒเคจ เคธเฅ€ เคธเฅเคตเคฟเคงเคพเคเค เคธเคš เคฎเฅ‡เค‚ เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เคนเฅ‹เคคเฅ€ เคนเฅˆเค‚เฅค เค‡เคธเคฎเฅ‡เค‚ เคเค• เคฌเฅ‡เคคเคฐเคคเฅ€เคฌ เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เค†เคˆเคกเฅ€, เค†เคชเค•เฅ‡ เคชเคพเคธ เค•เคฟเคคเคจเฅ‡ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เค”เคฐ เคนเฅ‹เคธเฅเคŸ เคนเฅˆเค‚, เคเคช เค•เคพ เคธเค‚เคธเฅเค•เคฐเคฃ, เค”เคฐ เคชเคฟเค›เคฒเฅ‡ 24 เค˜เค‚เคŸเฅ‡ เคฎเฅ‡เค‚ เค‡เคธเฅเคคเฅ‡เคฎเคพเคฒ เคนเฅเคˆ เคธเฅเคตเคฟเคงเคพเคเค (เคŸเคฐเฅเคฎเคฟเคจเคฒ, เคซเคผเคพเค‡เคฒ เคฎเฅˆเคจเฅ‡เคœเคฐ, เคŸเคจเคฒ, docker เค†เคฆเคฟ) เคนเฅ‹เคคเฅ€ เคนเฅˆเค‚เฅค เค‡เคธเคฎเฅ‡เค‚ เค•เคญเฅ€ เคญเฅ€ เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เคจเคพเคฎ, เคนเฅ‹เคธเฅเคŸ เคจเคพเคฎ, IP เคชเคคเฅ‡, เค•เฅเคฐเฅ‡เคกเฅ‡เค‚เคถเคฟเคฏเคฒ เคฏเคพ เคเคธเฅ€ เค•เฅ‹เคˆ เคšเฅ€เคœเคผ เคจเคนเฅ€เค‚ เคนเฅ‹เคคเฅ€ เคœเฅ‹ เค†เคชเค•เฅ€ เคฏเคพ เค†เคชเค•เฅ‡ เคธเคฐเฅเคตเคฐ เค•เฅ€ เคชเคนเคšเคพเคจ เคฌเคคเคพเคเฅค + +เคฏเคน เคกเคฟเคซเคผเฅ‰เคฒเฅเคŸ เคฐเฅ‚เคช เคธเฅ‡ เคšเคพเคฒเฅ‚ เคฐเคนเคคเคพ เคนเฅˆเฅค เค‡เคธเฅ‡ เคเคกเคฎเคฟเคจ เคธเฅ‡เคŸเคฟเค‚เค—เฅเคธ เคฎเฅ‡เค‚ เคธเคพเคฎเคพเคจเฅเคฏ เค•เฅ‡ เค…เค‚เคคเคฐเฅเค—เคค เคฌเค‚เคฆ เค•เคฐเฅ‡เค‚, เคฏเคพ Termix เคถเฅเคฐเฅ‚ เค•เคฐเคจเฅ‡ เคธเฅ‡ เคชเคนเคฒเฅ‡ เคนเฅ€ `ENABLE_TELEMETRY=false` เคธเฅ‡เคŸ เค•เคฐ เคฆเฅ‡เค‚เฅค +
## เคฆเคพเคจ เค•เคฐเฅ‡เค‚ -Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคฌเคฟเคจเคพ เค•เคฟเคธเฅ€ เคธเคฌเฅเคธเค•เฅเคฐเคฟเคชเฅเคถเคจ เคฏเคพ เคชเฅ‡เคก เคชเฅเคฒเคพเคจ เค•เฅ‡เฅค เคฏเคฆเคฟ เค†เคชเค•เฅ‹ เคฏเคน เค‰เคชเคฏเฅ‹เค—เฅ€ เคฒเค—เคคเคพ เคนเฅˆ, เคคเฅ‹ เคธเคฐเฅเคตเคฐ เคฒเคพเค—เคค, เคกเฅ‹เคฎเฅ‡เคจ เค”เคฐ เคตเคฟเค•เคพเคธ เคธเคฎเคฏ เค•เฅ‹ เค•เคตเคฐ เค•เคฐเคจเฅ‡ เคฎเฅ‡เค‚ เคฎเคฆเคฆ เค•เฅ‡ เคฒเคฟเค เคฆเคพเคจ เค•เคฐเคจเฅ‡ เคชเคฐ เคตเคฟเคšเคพเคฐ เค•เคฐเฅ‡เค‚เฅค เคฆเคพเคจ SAML, Kubernetes, เค”เคฐ Agent เคธเคชเฅ‹เคฐเฅเคŸ เคœเฅˆเคธเฅ€ เคธเฅเคตเคฟเคงเคพเค“เค‚ เค•เฅ‡ เคจเคฟเคฐเฅเคฎเคพเคฃ เค•เฅ‡ เคฒเคฟเค เค†เคตเคถเฅเคฏเค• เคถเฅ‹เคง เค”เคฐ เคธเฅ€เค–เคจเฅ‡ เคฎเฅ‡เค‚ เคฒเค—เคจเฅ‡ เคตเคพเคฒเฅ‡ เคธเคฎเคฏ เค•เฅ‹ เคตเคฟเคคเฅเคค เคชเฅ‹เคทเคฟเคค เค•เคฐเคจเฅ‡ เคฎเฅ‡เค‚ เคญเฅ€ เคฎเคฆเคฆ เค•เคฐเคคเฅ‡ เคนเฅˆเค‚เฅค เคจเฅ€เคšเฅ‡ เคชเฅเคฐเค—เคคเคฟ เคฆเฅ‡เค–เฅ‡เค‚ เค”เคฐ เคฆเคพเคจ เค•เคฐเฅ‡เค‚เฅค +Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคจ เค•เฅ‹เคˆ เคธเคฆเคธเฅเคฏเคคเคพ เคนเฅˆ เคจ เค•เฅ‹เคˆ เคชเฅ‡เคก เคชเฅเคฒเคพเคจเฅค เค…เค—เคฐ เคฏเคน เค†เคชเค•เฅ‡ เค•เคพเคฎ เค†เคคเคพ เคนเฅˆ, เคคเฅ‹ เคธเคฐเฅเคตเคฐ, เคกเฅ‹เคฎเฅ‡เคจ เค”เคฐ เคตเคฟเค•เคพเคธ เค•เฅ‡ เคธเคฎเคฏ เคฎเฅ‡เค‚ เคฎเคฆเคฆ เค•เฅ‡ เคฒเคฟเค เคฆเคพเคจ เค•เคฐเคจเฅ‡ เคชเคฐ เคตเคฟเคšเคพเคฐ เค•เคฐเฅ‡เค‚เฅค เคฆเคพเคจ เคธเฅ‡ SAML, Kubernetes เค”เคฐ เคเคœเฅ‡เค‚เคŸ เคธเคชเฅ‹เคฐเฅเคŸ เคœเฅˆเคธเฅ€ เคธเฅเคตเคฟเคงเคพเคเค เคฌเคจเคพเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคœเคผเคฐเฅ‚เคฐเฅ€ เคถเฅ‹เคง เค”เคฐ เคธเฅ€เค–เคจเฅ‡ เค•เคพ เคธเคฎเคฏ เคญเฅ€ เคฎเคฟเคฒเคคเคพ เคนเฅˆเฅค เคจเฅ€เคšเฅ‡ เคชเฅเคฐเค—เคคเคฟ เคฆเฅ‡เค–เฅ‡เค‚ เค”เคฐ เคฆเคพเคจ เค•เคฐเฅ‡เค‚เฅค [เคฆเคพเคจ เค•เคฐเฅ‡เค‚](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคฌเคฟเคจเคพ ## เคชเฅเคฐเคพเคฏเฅ‹เคœเค• -เคตเคฟเค•เคพเคธ เค•เฅ‹ เคธเคฎเคฐเฅเคฅเคจ เคฆเฅ‡เคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคชเฅ‡เคก เคชเฅเคฒเฅ‡เคธเคฎเฅ‡เค‚เคŸ เคฎเฅ‡เค‚ เคฐเฅเคšเคฟ เคนเฅˆ? [mail@termix.site](mailto:mail@termix.site) เคชเคฐ เคˆเคฎเฅ‡เคฒ เค•เคฐเฅ‡เค‚เฅค +เคตเคฟเค•เคพเคธ เคฎเฅ‡เค‚ เคธเคนเคฏเฅ‹เค— เค•เฅ‡ เคฒเคฟเค เคชเฅ‡เคก เคชเฅเคฒเฅ‡เคธเคฎเฅ‡เค‚เคŸ เคฎเฅ‡เค‚ เคฐเฅเคšเคฟ เคนเฅˆ? [mail@termix.site](mailto:mail@termix.site) เคชเคฐ เคˆเคฎเฅ‡เคฒ เค•เคฐเฅ‡เค‚เฅค
@@ -325,10 +410,6 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคฌเคฟเคจเคพ Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคฌเคฟเคจเคพ Rack Genius - +    + + Ginernet +

## เคธเคนเคพเคฏเคคเคพ -เคฏเคฆเคฟ เค†เคชเค•เฅ‹ เคธเคนเคพเคฏเคคเคพ เคšเคพเคนเคฟเค เคฏเคพ Termix เค•เฅ‡ เคฒเคฟเค เค•เคฟเคธเฅ€ เคตเคฟเคถเฅ‡เคทเคคเคพ เค•เคพ เค…เคจเฅเคฐเฅ‹เคง เค•เคฐเคจเคพ เคšเคพเคนเคคเฅ‡ เคนเฅˆเค‚, เคคเฅ‹ [เค‡เคถเฅเคฏเฅ‚เคœเคผ](https://github.com/Termix-SSH/Support/issues) เคชเฅ‡เคœ เคชเคฐ เคœเคพเคเค, เคฒเฅ‰เค— เค‡เคจ เค•เคฐเฅ‡เค‚, เค”เคฐ `New Issue` เคฆเคฌเคพเคเคเฅค เค•เฅƒเคชเคฏเคพ เค…เคชเคจเฅ‡ เค‡เคถเฅเคฏเฅ‚ เคฎเฅ‡เค‚ เคฏเคฅเคพเคธเค‚เคญเคต เคตเคฟเคธเฅเคคเฅƒเคค เคตเคฟเคตเคฐเคฃ เคฆเฅ‡เค‚, เค…เคงเคฟเคฎเคพเคจเคคเคƒ เค…เค‚เค—เฅเคฐเฅ‡เคœเคผเฅ€ เคฎเฅ‡เค‚ เคฒเคฟเค–เฅ‡เค‚เฅค เค†เคช [Discord](https://discord.gg/jVQGdvHDrf) เคธเคฐเฅเคตเคฐ เคฎเฅ‡เค‚ เคญเฅ€ เคถเคพเคฎเคฟเคฒ เคนเฅ‹ เคธเค•เคคเฅ‡ เคนเฅˆเค‚ เค”เคฐ เคธเคนเคพเคฏเคคเคพ เคšเฅˆเคจเคฒ เคชเคฐ เคœเคพ เคธเค•เคคเฅ‡ เคนเฅˆเค‚, เคนเคพเคฒเคพเคเค•เคฟ, เคชเฅเคฐเคคเคฟเค•เฅเคฐเคฟเคฏเคพ เคธเคฎเคฏ เค…เคงเคฟเค• เคนเฅ‹ เคธเค•เคคเคพ เคนเฅˆเฅค +เคฎเคฆเคฆ เคšเคพเคนเคฟเค เคฏเคพ เค•เฅ‹เคˆ เคธเฅเคตเคฟเคงเคพ เคฎเคพเคเค—เคจเฅ€ เคนเฅˆ? เคเค• [เคจเคฏเคพ issue](https://github.com/Termix-SSH/Support/issues) เค–เฅ‹เคฒเฅ‡เค‚ เค”เคฐ เคœเคฟเคคเคจเคพ เคนเฅ‹ เคธเค•เฅ‡ เคตเคฟเคธเฅเคคเคพเคฐ เคธเฅ‡ เคฒเคฟเค–เฅ‡เค‚, เคนเฅ‹ เคธเค•เฅ‡ เคคเฅ‹ เค…เค‚เค—เฅเคฐเฅ‡เคœเคผเฅ€ เคฎเฅ‡เค‚เฅค เค†เคช [Discord](https://discord.gg/jVQGdvHDrf) เค•เฅ‡ เคธเคชเฅ‹เคฐเฅเคŸ เคšเฅˆเคจเคฒ เคฎเฅ‡เค‚ เคญเฅ€ เคชเฅ‚เค› เคธเค•เคคเฅ‡ เคนเฅˆเค‚, เคนเคพเคฒเคพเคเค•เคฟ เคตเคนเคพเค เคœเคตเคพเคฌ เค†เคจเฅ‡ เคฎเฅ‡เค‚ เคœเคผเฅเคฏเคพเคฆเคพ เคธเคฎเคฏ เคฒเค— เคธเค•เคคเคพ เคนเฅˆเฅค
@@ -359,7 +443,7 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคฌเคฟเคจเคพ [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -YouTube เคชเคฐ เค…เคชเคกเฅ‡เคŸ เค•เฅ€ เคธเคฎเฅ€เค•เฅเคทเคพเคเค เคฆเฅ‡เค–เฅ‡เค‚ +YouTube เคชเคฐ เค…เคชเคกเฅ‡เคŸ เค•เฅ€ เคœเคพเคจเค•เคพเคฐเฅ€ เคฆเฅ‡เค–เฅ‡เค‚

@@ -399,7 +483,7 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคฌเคฟเคจเคพ -เค•เฅเค› เคตเฅ€เคกเคฟเคฏเฅ‹ เค”เคฐ เค›เคตเคฟเคฏเคพเค เคชเฅเคฐเคพเคจเฅ€ เคนเฅ‹ เคธเค•เคคเฅ€ เคนเฅˆเค‚ เคฏเคพ เคตเคฟเคถเฅ‡เคทเคคเคพเค“เค‚ เค•เฅ‹ เคชเฅ‚เคฐเฅ€ เคคเคฐเคน เคธเฅ‡ เคชเฅเคฐเคฆเคฐเฅเคถเคฟเคค เคจเคนเฅ€เค‚ เค•เคฐ เคธเค•เคคเฅ€ เคนเฅˆเค‚เฅค +เค•เฅเค› เคตเฅ€เคกเคฟเคฏเฅ‹ เค”เคฐ เคคเคธเฅเคตเฅ€เคฐเฅ‡เค‚ เคชเฅเคฐเคพเคจเฅ€ เคนเฅ‹ เคธเค•เคคเฅ€ เคนเฅˆเค‚ เคฏเคพ เคธเฅเคตเคฟเคงเคพเค“เค‚ เค•เฅ‹ เคชเฅ‚เคฐเฅ€ เคคเคฐเคน เคจเคนเฅ€เค‚ เคฆเคฟเค–เคพ เคชเคพเคคเฅ€เค‚เฅค @@ -407,10 +491,10 @@ Termix เคฎเฅเคซเคผเฅเคค เค”เคฐ เค“เคชเคจ เคธเฅ‹เคฐเฅเคธ เคนเฅˆ, เคฌเคฟเคจเคพ ## เคจเคฟเคฏเฅ‹เคœเคฟเคค เคตเคฟเคถเฅ‡เคทเคคเคพเคเค -เคธเคญเฅ€ เคจเคฟเคฏเฅ‹เคœเคฟเคค เคตเคฟเคถเฅ‡เคทเคคเคพเค“เค‚ เค•เฅ‡ เคฒเคฟเค [เคชเฅเคฐเฅ‹เคœเฅ‡เค•เฅเคŸเฅเคธ](https://github.com/orgs/Termix-SSH/projects/5) เคฆเฅ‡เค–เฅ‡เค‚เฅค เคฏเคฆเคฟ เค†เคช เคฏเฅ‹เค—เคฆเคพเคจ เคฆเฅ‡เคจเคพ เคšเคพเคนเคคเฅ‡ เคนเฅˆเค‚, เคคเฅ‹ [เคฏเฅ‹เค—เคฆเคพเคจ](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) เคฆเฅ‡เค–เฅ‡เค‚เฅค +เคธเคญเฅ€ เคจเคฟเคฏเฅ‹เคœเคฟเคค เคธเฅเคตเคฟเคงเคพเคเค [Projects](https://github.com/orgs/Termix-SSH/projects/5) เคฎเฅ‡เค‚ เคนเฅˆเค‚เฅค เค…เค—เคฐ เค†เคช เคฏเฅ‹เค—เคฆเคพเคจ เคฆเฅ‡เคจเคพ เคšเคพเคนเคคเฅ‡ เคนเฅˆเค‚, เคคเฅ‹ [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) เคฆเฅ‡เค–เฅ‡เค‚เฅค
## เคฒเคพเค‡เคธเฅ‡เค‚เคธ -Apache License Version 2.0 เค•เฅ‡ เคคเคนเคค เคตเคฟเคคเคฐเคฟเคคเฅค เค…เคงเคฟเค• เคœเคพเคจเค•เคพเคฐเฅ€ เค•เฅ‡ เคฒเคฟเค `LICENSE` เคฆเฅ‡เค–เฅ‡เค‚เฅค +Apache License เคธเค‚เคธเฅเค•เคฐเคฃ 2.0 เค•เฅ‡ เคคเคนเคค เคตเคฟเคคเคฐเคฟเคคเฅค เค…เคงเคฟเค• เคœเคพเคจเค•เคพเคฐเฅ€ เค•เฅ‡ เคฒเคฟเค `LICENSE` เคฆเฅ‡เค–เฅ‡เค‚เฅค diff --git a/docs/readme/README-IT.md b/docs/readme/README-IT.md index 91d6884..d38ab41 100644 --- a/docs/readme/README-IT.md +++ b/docs/readme/README-IT.md @@ -4,7 +4,7 @@

Termix

-

Gestione SSH self-hosted e accesso al desktop remoto

+

Gestione dei server self-hosted, da SSH e desktop remoto fino alle automazioni

English ยท @@ -32,12 +32,12 @@

- Donazioni di questo mese + Donations this month


-Termix รจ gratuito e open source. Se lo trovi utile, considera di [donare](https://donate.termix.site/) per aiutare a coprire i costi del server e il tempo di sviluppo. +Termix รจ gratuito e open source. Se ti รจ utile, valuta una [donazione](https://donate.termix.site/) per aiutare a coprire i costi dei server e il tempo di sviluppo.
@@ -58,7 +58,7 @@ Termix รจ gratuito e open source. Se lo trovi utile, considera di [donare](https ## Panoramica -Termix รจ una piattaforma di gestione server tutto-in-uno, open-source, per sempre gratuita e self-hosted. Fornisce una soluzione multipiattaforma per gestire i tuoi server e la tua infrastruttura attraverso un'unica interfaccia intuitiva. Termix offre accesso al terminale SSH, controllo remoto del desktop (RDP, VNC, Telnet), funzionalitร  di tunneling SSH, gestione remota dei file e molti altri strumenti. Termix รจ la perfetta alternativa gratuita e self-hosted a Termius, disponibile per tutte le piattaforme. +Termix รจ una piattaforma gratuita, open source e self-hosted per gestire i tuoi server. Mette in un unico posto terminali SSH, desktop remoti (RDP, VNC, Telnet), trasferimenti di file, tunnel, Docker, metriche e automazioni, su web, desktop e mobile. รˆ un'alternativa self-hosted a Termius che resta gratuita per sempre.
@@ -68,126 +68,182 @@ Termix รจ una piattaforma di gestione server tutto-in-uno, open-source, per semp -**Accesso Terminale SSH:** -Terminale completo con supporto schermo diviso (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni. +**Terminale SSH:** +Un terminale completo con schede come quelle del browser e schermo diviso, fino a 6 pannelli insieme. Scegli tema, carattere e colori. Sopra ogni sessione c'รจ una barra con CPU, memoria e disco in tempo reale, piรน scorciatoie ai file, a Docker, ai tunnel e alle metriche di quell'host. -**Accesso Desktop Remoto:** -Supporto RDP, VNC e Telnet tramite browser con personalizzazione completa e schermo diviso. +**Desktop remoto:** +RDP, VNC e Telnet nel browser, in schede e schermo diviso come qualsiasi altra sessione. Include un browser dei file per le unitร  RDP e il caricamento trascinando i file. Sul desktop Windows puoi anche aprire un host nel client RDP nativo. -**Gestione Tunnel SSH:** -Crea e gestisci tunnel SSH da server a server con riconnessione automatica, monitoraggio dello stato e inoltro locale, remoto o SOCKS dinamico. Le impostazioni del tunnel da client desktop a server sono archiviate localmente per ogni installazione desktop; gli snapshot di preset C2S opzionali possono essere salvati sul server, rinominati, caricati o eliminati per spostare una configurazione di tunnel locale tra i client. +**Tunnel SSH:** +Inoltro locale, remoto e SOCKS dinamico, con riconnessione automatica e controlli di stato. I tunnel da client a server dell'app desktop restano su quella macchina, e puoi salvare delle preimpostazioni sul server per portare una configurazione su un altro computer. -**Gestore File Remoto:** -Gestisci i file direttamente sui server remoti con supporto per la visualizzazione e la modifica di codice, immagini, audio e video. Carica, scarica, rinomina, elimina e sposta file senza problemi con supporto sudo. Include il supporto per spostare file da server a server. +**Gestore file:** +Sfoglia, modifica, carica, scarica, rinomina, sposta ed elimina file via SFTP, anche con sudo. Guarda e modifica codice, immagini, audio e video. Copia i file direttamente da un server all'altro: il percorso piรน veloce viene scelto per te e i trasferimenti vengono verificati. -**Gestione Docker e Podman:** -Avvia, ferma, metti in pausa, rimuovi container. Visualizza le statistiche dei container. Controlla i container tramite terminale docker exec. Supporta sia Docker che Podman come runtime dei container. Non รจ stato creato per sostituire Portainer o Dockge, ma piuttosto per gestire semplicemente i tuoi container rispetto alla loro creazione. +**Docker e Podman:** +Avvia, ferma, metti in pausa ed elimina i container, guarda le loro statistiche e apri una shell dentro uno di essi. Funziona sia con Docker sia con Podman. Non vuole sostituire Portainer o Dockge, serve solo a gestire i container che hai giร . -**Gestore Host SSH:** -Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle (con personalizzazione delle cartelle e supporto per cartelle annidate), salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH. +**Gestore host:** +Salva e organizza gli host con etichette e cartelle annidate a cui puoi dare nome e colore. Riutilizza le credenziali salvate su piรน host, distribuisci le chiavi SSH in automatico, raggruppa gli host sotto un host padre, modifica ed esporta in blocco, e usa la connessione rapida per i collegamenti una tantum che non vuoi salvare. -**Metriche Host:** -Visualizza CPU, memoria, utilizzo del disco, rete, uptime, informazioni di sistema, firewall, monitoraggio porte, visualizzatore di log, utenti/permessi, certificati e molto altro, funzionanti sulla maggior parte dei server basati su Linux. Include grafici storici delle serie temporali e avvisi basati su soglie con supporto ntfy e webhook. +**Metriche host:** +CPU, memoria, disco, rete, temperatura, tempo di accensione, processi, porte, accessi e informazioni di sistema sulla maggior parte dei server Linux, con grafici storici. Le schede di gestione ti fanno seguire servizi, cron, pacchetti, utenti, regole del firewall, WireGuard, Tailscale, certificati SSL, log e controlli di stato senza uscire da Termix. -**Autenticazione Utente:** -Gestione utenti sicura con controlli amministrativi (puรฒ modificare le informazioni di altri utenti) e OIDC/LDAP/SSO (con controllo degli accessi), 2FA (TOTP) e supporto passkey (WebAuthn). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro. Visualizza il log di controllo delle azioni di tutti gli utenti. +**Automazioni:** +Scegli un evento che fa partire tutto, poi decidi cosa deve succedere. Gli eventi possono essere una metrica che supera una soglia, un host che cade o torna su, un controllo di stato che cambia, una pianificazione, un evento di un container o un webhook in arrivo. I passaggi possono eseguire comandi e frammenti, gestire container e tunnel, accendere un host, chiamare un URL, aspettare, seguire una condizione, avviare un'altra automazione e avvisarti via ntfy, Discord o webhook. Le prove a vuoto ti permettono di provare senza rischi. -**Integrazione Tailscale:** -Elenca i dispositivi della tua rete Tailscale per aggiungerli rapidamente come host, e connettiti utilizzando Tailscale SSH come metodo di autenticazione, lasciando che le ACL della tua rete gestiscano l'autorizzazione senza memorizzare credenziali. +**Flotte:** +Raggruppa gli host in una flotta scegliendoli o con regole sulle etichette, cosรฌ i nuovi host entrano da soli. Esegui un comando su tutti gli host in una volta, invia e recupera file su tutti quanti, installa pacchetti e raccogli un inventario di sistema operativo, kernel, architettura e tempo di accensione. -**RBAC/Condivisione:** -Crea ruoli e condividi host tra utenti/ruoli. Supporta tutti i tipi di autenticazione e tutti i protocolli host. +**Assistente IA:** +รˆ opzionale e resta spento finchรฉ non lo accendi tu. Collega OpenAI, Anthropic, Gemini, Ollama o qualsiasi endpoint compatibile con OpenAI e fai domande sulla tua installazione. Legge host, flotte, frammenti e avvisi, e propone modifiche da approvare invece di farle da solo. Non puรฒ mai toccare credenziali, utenti o impostazioni. Gli amministratori possono lasciarlo spento per tutta l'istanza, e tu puoi nasconderlo giร  durante la configurazione. -**Connessioni Seriali:** -Connettiti a dispositivi seriali (router, switch, microcontrollori, ecc.) direttamente dal browser o dall'app desktop. Configura baud rate, bit di dati, bit di stop e paritร . Utilizza la Web Serial API nei browser supportati o un backend nativo nell'app Electron. +**Accesso e utenti:** +Account locali piรน accesso con OIDC, LDAP, GitHub e Google, con doppia autenticazione (TOTP), passkey (WebAuthn) e dispositivi fidati. Gli amministratori possono gestire gli utenti, collegare i gruppi OIDC ai ruoli, vedere tutte le sessioni attive su ogni piattaforma e revocarle. Collega il tuo account locale a quello OIDC e consulta il registro di controllo di quello che ha fatto ognuno. +**Ruoli e condivisione:** +Crea ruoli e condividi gli host con utenti o ruoli su quattro livelli: connessione, visualizzazione, modifica e gestione. Funziona con ogni tipo di autenticazione e ogni protocollo, e puoi cambiare le credenziali usate per un host condiviso. + + + + + + **Avvisi:** -Imposta regole di avviso basate su soglie per le metriche dell'host (CPU, memoria, disco, ecc.) e ricevi notifiche tramite ntfy o webhook quando si attivano. Visualizza gli avvisi attivi e risolti in un registro storico. +Imposta regole sulle metriche degli host come CPU, memoria e disco, e ricevi una notifica via ntfy, Discord o webhook quando scattano. Guarda gli avvisi attivi e quelli rientrati in uno storico, e scarta quelli che non ti interessano. + + + + +**Pagina iniziale:** +Una griglia di widget che costruisci tu trascinandoli. Ci sono widget per stato degli host, ping, collegamenti ai servizi, segnalibri, ricerca, orologi, calendari, conti alla rovescia, note, RSS, meteo, immagini, iframe, Docker, tunnel, grafici delle metriche, API personalizzate e perfino un terminale dal vivo. -**Homepage:** -Una homepage completamente personalizzabile con una griglia di widget drag-and-drop. Aggiungi widget per lo stato dell'host, link ai servizi, orologi, note, feed RSS, meteo, container Docker, grafici delle metriche dell'host, terminali incorporati, iframe e altro ancora. +**Frammenti e strumenti:** +Salva i comandi che usi spesso e lanciali con un clic, con variabili per l'host e per quello che scrivi tu. Esegui uno stesso comando su tutti i terminali aperti e cerca nella cronologia con il completamento automatico. -**Crittografia Database:** -Il backend รจ archiviato come file di database SQLite crittografati. Consulta la [documentazione](https://docs.termix.site/security) per maggiori informazioni. +**Condivisione sessione:** +Condividi dal vivo una sessione di terminale, RDP, VNC o Telnet. Manda un link a cui chiunque puรฒ accedere senza account, oppure condividi con un utente Termix preciso, in sola lettura o anche in scrittura. Le condivisioni possono scadere da sole o essere revocate, e si possono spegnere per tutti o per singolo host. -**Grafico di Rete:** -Personalizza la tua Dashboard per visualizzare il tuo homelab basato sulle connessioni SSH con supporto dello stato. +**Registrazione e log delle sessioni:** +Registra le sessioni di terminale, RDP e VNC e riguardale dopo. Scarica i log di testo di una sessione e consulta il registro delle connessioni per vedere esattamente cosa รจ successo durante una connessione. -**Strumenti SSH:** -Crea snippet di comandi riutilizzabili che si eseguono con un singolo clic. Esegui un comando simultaneamente su piรน terminali aperti. +**Connessioni seriali:** +Parla con dispositivi seriali come router, switch e microcontrollori dal browser o dall'app desktop. Imposta velocitร , bit di dati, bit di stop e paritร . Usa l'API Web Serial nei browser compatibili, oppure un backend nativo nell'app desktop. -**Schede Persistenti:** -Le sessioni SSH e le schede rimangono aperte tra dispositivi/aggiornamenti se abilitato nel profilo utente. +**Tailscale:** +Prendi i dispositivi dalla tua tailnet per aggiungerli come host in pochi clic, e collegati con Tailscale SSH cosรฌ gli ACL della tailnet gestiscono l'accesso senza salvare credenziali. Funzionano anche Headscale e gli endpoint personalizzati. + + + + +**Proxmox:** +Importa gli host direttamente da un'istanza Proxmox e segui le statistiche di nodi e macchine ospiti, comprese CPU, memoria e spazio, in una scheda dedicata. + + + + + + +**Spazi di lavoro e schede:** +Salva un insieme di schede con la loro disposizione divisa e riapri tutto con un clic. Termix ricorda anche l'ultima sessione, cosรฌ le schede tornano dopo un ricaricamento e su altri dispositivi. + + + + +**Configurazione guidata:** +Una breve configurazione ti accompagna nella scelta di una preimpostazione dell'interfaccia, del tema, delle funzionalitร  che vuoi e del primo host. La modalitร  semplice nasconde quello che non usi, e puoi rifare la configurazione o cambiare preimpostazione quando vuoi. + + + + + + +**Desktop autonomo e sincronizzazione:** +L'app desktop funziona da sola, con backend e database locali, senza bisogno di un server. Puoi anche collegarla a un server Termix per sincronizzare nei due sensi host, credenziali, frammenti e altro, e scegliere se le connessioni partono dal tuo computer o passano dal server. + + + + +**Riga di comando:** +Una CLI `termix` per la tua shell e i tuoi script. Apri terminali, esegui un comando su un host o su un'intera flotta, sposta file via SFTP e gestisci host, frammenti e credenziali. Installala con `npm install -g @termix-cli/cli` oppure prendi un binario autonomo. Vedi la [documentazione della CLI](https://docs.termix.site/cli). + + + + + + +**Sicurezza:** +Password, chiavi e altri segreti sono cifrati per ogni utente, e gli stessi file del database possono essere cifrati su disco. Guarda la [documentazione](https://docs.termix.site/security) per capire come funziona. **Lingue:** -Supporto integrato per circa 30 lingue (gestito da [Crowdin](https://docs.termix.site/translations)). +Circa 30 lingue incluse, gestite tramite [Crowdin](https://docs.termix.site/translations). @@ -199,23 +255,26 @@ Supporto integrato per circa 30 lingue (gestito da [Crowdin](https://docs.termix Altre funzionalitร 
-- **Dashboard** - Visualizza le informazioni del server a colpo d'occhio sulla tua dashboard -- **Chiavi API** - Crea chiavi API con ambito utente e date di scadenza da utilizzare per automazione/CI -- **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file -- **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS -- **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra molti temi UI diversi, inclusi chiaro, scuro, Dracula, ecc. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero. -- **Cronologia Comandi** - Autocompletamento e visualizzazione dei comandi SSH eseguiti in precedenza -- **Connessione Rapida** - Connettiti a un server senza dover salvare i dati di connessione -- **Palette Comandi** - Premi due volte shift sinistro per accedere rapidamente alle connessioni SSH con la tastiera -- **Integrazione Proxmox** - Aggiungi automaticamente host a Termix dalla tua istanza Proxmox -- **SSH Ricco di Funzionalitร ** - Supporta jump host, Warpgate, connessioni basate su TOTP, SOCKS5, verifica chiave host, compilazione automatica password, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, registrazione terminale, SSH agent forwarding, Bitwarden SSH agent, firma SSH HashiCorp Vault e altro ancora -- **Termix ID** - L'equivalente di sshid.io integrato in Termix. Rivendica un handle, pubblica le tue chiavi SSH pubbliche su un URL resolver e utilizza una CA integrata per emettere certificati SSH +- **Dashboard** - I tuoi server a colpo d'occhio, con schede che disponi tu +- **Grafico di rete** - Il tuo homelab disegnato a partire dagli host, con stato in tempo reale +- **Monitor tmux** - Sfoglia sessioni, finestre e pannelli di tmux, con anteprime e ricerca +- **Chiavi API** - Chiavi per singolo utente con scadenza, per script e CI +- **Esporta e importa** - Sposta host, credenziali e dati del gestore file dentro e fuori +- **SSL automatico** - Certificati generati e rinnovati per te, con reindirizzamento a HTTPS, oppure usa i tuoi +- **Database** - SQLite di base, con supporto anche per PostgreSQL e MySQL +- **Interfaccia moderna** - Interfaccia React pulita che funziona su desktop e mobile, con temi come chiaro, scuro e Dracula. Ogni connessione si puรฒ aprire a schermo intero da un URL +- **Palette comandi** - Premi due volte Maiusc sinistro per saltare a un host da tastiera +- **Scorciatoie da tastiera** - Spostarsi tra le schede, chiuderle e altro, tutto riassegnabile +- **Wake-on-LAN** - Accendi una macchina da Termix o da un passaggio di un'automazione +- **Autenticazione tramite proxy fidato** - Lascia che un reverse proxy gestisca l'accesso e passi l'utente +- **SSH molto completo** - Host di salto, Warpgate, richieste TOTP, SOCKS5, verifica delle chiavi host, riempimento automatico della password, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, log del terminale, inoltro dell'agente, agente SSH di Bitwarden, firma SSH con HashiCorp Vault e altro +- **Termix ID** - Una versione integrata di sshid.io. Prendi un identificativo, pubblica le tue chiavi pubbliche su un URL di risoluzione ed emetti certificati SSH dalla CA integrata
-## Supporto Piattaforme +## Piattaforme supportate @@ -224,15 +283,15 @@ Supporto integrato per circa 30 lingue (gestito da [Crowdin](https://docs.termix - + - + - + @@ -252,9 +311,9 @@ Supporto integrato per circa 30 lingue (gestito da [Crowdin](https://docs.termix ## Installazione -Visita la [Documentazione Termix](https://docs.termix.site/install) per le istruzioni complete di installazione su tutte le piattaforme. +Vai alla [documentazione di Termix](https://docs.termix.site/install) per le istruzioni complete di installazione su tutte le piattaforme. -File Docker Compose di esempio (puoi omettere `guacd` e la rete se non prevedi di utilizzare le funzioni di desktop remoto): +Esempio di file Docker Compose (puoi togliere `guacd` e la rete se non pensi di usare il desktop remoto): ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### Riga di comando + +Termix ha anche una CLI, cosรฌ puoi gestire i tuoi server dal terminale e usare Termix nei tuoi script. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +Puรฒ aprire terminali, eseguire un comando su un host o su un'intera flotta, spostare file via SFTP e gestire host, frammenti e credenziali. La documentazione completa รจ su [docs.termix.site/cli](https://docs.termix.site/cli). + +### Hosting in cloud + +Puoi far girare il server Termix su un VPS invece che dentro la tua rete. Se Termix gira sulla rete che gestisce, un guasto se lo porta via proprio quando ti servirebbe per sistemare le cose. Fuori resta raggiungibile, ti dร  un IP fisso e ci entri da ovunque senza VPN nรฉ porte aperte. + +[GINERNET](https://docs.termix.site/install/ginernet) sponsorizza Termix, e nella documentazione c'รจ una guida passo passo per il rilascio sulla loro piattaforma VPS. + +
+ +## Telemetria + +Termix invia una volta al giorno un piccolo segnale anonimo, cosรฌ posso vedere quante istanze sono attive e quali funzionalitร  vengono usate davvero. Contiene un ID istanza casuale, quanti utenti e host hai, la versione dell'app e quali funzionalitร  (terminale, gestore file, tunnel, docker, ecc.) sono state usate nelle ultime 24 ore. Non contiene mai nomi utente, nomi host, indirizzi IP, credenziali o qualsiasi altra cosa che identifichi te o i tuoi server. + +รˆ attivo di base. Puoi spegnerlo nelle impostazioni di amministrazione, sezione Generale, oppure impostare `ENABLE_TELEMETRY=false` prima ancora di avviare Termix. +
## Dona -Termix รจ gratuito e open source, senza abbonamenti o piani a pagamento. Se lo trovi utile, considera di donare per aiutare a coprire i costi del server, i domini e il tempo di sviluppo. Le donazioni aiutano anche a finanziare il tempo necessario per ricercare e imparare ciรฒ che serve per costruire funzionalitร  come SAML, Kubernetes e supporto Agent. Segui i progressi e dona qui sotto. +Termix รจ gratuito e open source, senza abbonamenti nรฉ piani a pagamento. Se ti รจ utile, valuta una donazione per aiutare con server, domini e tempo di sviluppo. Le donazioni finanziano anche il tempo per studiare quello che serve a costruire funzionalitร  come SAML, Kubernetes e il supporto agli agenti. Segui i progressi e dona qui sotto. [Dona](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix รจ gratuito e open source, senza abbonamenti o piani a pagamento. Se lo t ## Sponsor -Interessato a un posizionamento a pagamento per supportare lo sviluppo? Scrivi a [mail@termix.site](mailto:mail@termix.site). +Ti interessa uno spazio a pagamento per sostenere lo sviluppo? Scrivi a [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Interessato a un posizionamento a pagamento per supportare lo sviluppo? Scrivi a Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Interessato a un posizionamento a pagamento per supportare lo sviluppo? Scrivi a Rack Genius - +    + + Ginernet +

## Supporto -Se hai bisogno di aiuto o vuoi richiedere una funzionalitร  per Termix, visita la pagina [Issues](https://github.com/Termix-SSH/Support/issues), accedi e premi `New Issue`. Per favore, sii il piรน dettagliato possibile nella tua segnalazione, preferibilmente scritta in inglese. Puoi anche unirti al server [Discord](https://discord.gg/jVQGdvHDrf) e visitare il canale di supporto, tuttavia i tempi di risposta potrebbero essere piรน lunghi. +Ti serve aiuto o vuoi proporre una funzionalitร ? Apri una [nuova issue](https://github.com/Termix-SSH/Support/issues) con piรน dettagli possibile, in inglese se ci riesci. Puoi anche chiedere nel canale di supporto su [Discord](https://discord.gg/jVQGdvHDrf), anche se lรฌ le risposte possono richiedere piรน tempo.
@@ -399,18 +483,18 @@ Se hai bisogno di aiuto o vuoi richiedere una funzionalitร  per Termix, visita l
WebQualsiasi browser moderno (Chrome, Safari, Firefox) ยท Supporto PWAQualsiasi browser recente (Chrome, Safari, Firefox) ยท Supporto PWA
Windows x64/ia32Portable ยท Installer MSI ยท ChocolateyPortatile ยท Installer MSI ยท Chocolatey
Linux x64/ia32Portable ยท AUR ยท AppImage ยท Deb ยท FlatpakPortatile ยท AUR ยท AppImage ยท Deb ยท Flatpak
macOS x64/ia32, v12.0+
-Alcuni video e immagini potrebbero non essere aggiornati o potrebbero non mostrare perfettamente le funzionalitร . +Alcuni video e immagini possono essere datati o non mostrare al meglio le funzionalitร .
-## Funzionalitร  Pianificate +## Funzionalitร  pianificate -Consulta [Projects](https://github.com/orgs/Termix-SSH/projects/5) per tutte le funzionalitร  pianificate. Se desideri contribuire, consulta [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +Tutte le funzionalitร  pianificate sono su [Projects](https://github.com/orgs/Termix-SSH/projects/5). Se vuoi contribuire, guarda [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
## Licenza -Distribuito sotto la Licenza Apache Versione 2.0. Consulta `LICENSE` per maggiori informazioni. +Distribuito con licenza Apache versione 2.0. Vedi `LICENSE` per maggiori informazioni. diff --git a/docs/readme/README-JA.md b/docs/readme/README-JA.md index dbde058..0f5a2c4 100644 --- a/docs/readme/README-JA.md +++ b/docs/readme/README-JA.md @@ -4,7 +4,7 @@

Termix

-

ใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆๅž‹ SSH ็ฎก็†ใจใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใ‚ฏใ‚ปใ‚น

+

SSH ใ‚„ใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‹ใ‚‰่‡ชๅ‹•ๅŒ–ใพใงใ€ใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆใฎใ‚ตใƒผใƒใƒผ็ฎก็†

English ยท @@ -37,7 +37,7 @@
-Termix ใฏ็„กๆ–™ใฎใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใงใ™ใ€‚ไพฟๅˆฉใ ใจๆ„Ÿใ˜ใŸๅ ดๅˆใฏใ€ใ‚ตใƒผใƒใƒผใ‚ณใ‚นใƒˆใจ้–‹็™บๆ™‚้–“ใฎใŸใ‚ใซ[ๅฏ„ไป˜](https://donate.termix.site/)ใ‚’ใ”ๆคœ่จŽใใ ใ•ใ„ใ€‚ +Termix ใฏ็„กๆ–™ใงใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใงใ™ใ€‚ๅฝนใซ็ซ‹ใฃใŸใจๆ„Ÿใ˜ใŸใ‚‰ใ€ใ‚ตใƒผใƒใƒผ่ฒป็”จใจ้–‹็™บๆ™‚้–“ใ‚’ๆ”ฏใˆใ‚‹ใŸใ‚ใซ[ๅฏ„ไป˜](https://donate.termix.site/)ใ‚’ใ”ๆคœ่จŽใใ ใ•ใ„ใ€‚
@@ -49,7 +49,7 @@ Termix ใฏ็„กๆ–™ใฎใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใงใ™ใ€‚ไพฟๅˆฉใ ใจ

Repo of the Day Achievement
- 2025ๅนด9ๆœˆ1ๆ—ฅใซ้”ๆˆ + 2025ๅนด9ๆœˆ1ๆ—ฅ ้”ๆˆ

@@ -58,7 +58,7 @@ Termix ใฏ็„กๆ–™ใฎใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใงใ™ใ€‚ไพฟๅˆฉใ ใจ ## ๆฆ‚่ฆ -Termixใฏใ€ใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใงๆฐธไน…็„กๆ–™ใฎใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆๅž‹ใ‚ชใƒผใƒซใ‚คใƒณใƒฏใƒณใ‚ตใƒผใƒใƒผ็ฎก็†ใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ใงใ™ใ€‚ๅ˜ไธ€ใฎ็›ดๆ„Ÿ็š„ใชใ‚คใƒณใ‚ฟใƒผใƒ•ใ‚งใƒผใ‚นใ‚’้€šใ˜ใฆใ€ใ‚ตใƒผใƒใƒผใจใ‚คใƒณใƒ•ใƒฉใ‚นใƒˆใƒฉใ‚ฏใƒใƒฃใ‚’็ฎก็†ใ™ใ‚‹ใƒžใƒซใƒใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ใ‚ฝใƒชใƒฅใƒผใ‚ทใƒงใƒณใ‚’ๆไพ›ใ—ใพใ™ใ€‚Termixใฏใ€SSHใ‚ฟใƒผใƒŸใƒŠใƒซใ‚ขใ‚ฏใ‚ปใ‚นใ€ใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ๅˆถๅพก๏ผˆRDPใ€VNCใ€Telnet๏ผ‰ใ€SSHใƒˆใƒณใƒใƒชใƒณใ‚ฐๆฉŸ่ƒฝใ€ใƒชใƒขใƒผใƒˆใƒ•ใ‚กใ‚คใƒซ็ฎก็†ใ€ใŠใ‚ˆใณใใฎไป–ๅคšใใฎใƒ„ใƒผใƒซใ‚’ๆไพ›ใ—ใพใ™ใ€‚Termixใฏใ€ใ™ในใฆใฎใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ใงๅˆฉ็”จๅฏ่ƒฝใชTermiusใฎๅฎŒๅ…จ็„กๆ–™ใงใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆๅฏ่ƒฝใชไปฃๆ›ฟใ‚ฝใƒชใƒฅใƒผใ‚ทใƒงใƒณใงใ™ใ€‚ +Termix ใฏ็„กๆ–™ใงใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใฎใ€ใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆๅž‹ใ‚ตใƒผใƒใƒผ็ฎก็†ใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ใงใ™ใ€‚SSH ใ‚ฟใƒผใƒŸใƒŠใƒซใ€ใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—๏ผˆRDPใ€VNCใ€Telnet๏ผ‰ใ€ใƒ•ใ‚กใ‚คใƒซ่ปข้€ใ€ใƒˆใƒณใƒใƒซใ€Dockerใ€ใƒกใƒˆใƒชใ‚ฏใ‚นใ€่‡ชๅ‹•ๅŒ–ใ‚’ใฒใจใคใซใพใจใ‚ใ€ใ‚ฆใ‚งใƒ–ใ€ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ€ใƒขใƒใ‚คใƒซใงไฝฟใˆใพใ™ใ€‚ใšใฃใจ็„กๆ–™ใงไฝฟใˆใ‚‹ใ€ใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆ็‰ˆใฎ Termius ไปฃๆ›ฟใงใ™ใ€‚
@@ -68,42 +68,42 @@ Termixใฏใ€ใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใงๆฐธไน…็„กๆ–™ใฎใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆๅž‹ใ‚ชใƒผ -**SSHใ‚ฟใƒผใƒŸใƒŠใƒซใ‚ขใ‚ฏใ‚ปใ‚น:** -ใƒ–ใƒฉใ‚ฆใ‚ถ้ขจใ‚ฟใƒ–ใ‚ทใ‚นใƒ†ใƒ ใซใ‚ˆใ‚‹ๅˆ†ๅ‰ฒ็”ป้ขๅฏพๅฟœ๏ผˆๆœ€ๅคง4ใƒ‘ใƒใƒซ๏ผ‰ใฎใƒ•ใƒซๆฉŸ่ƒฝใ‚ฟใƒผใƒŸใƒŠใƒซใ€‚ไธ€่ˆฌ็š„ใชใ‚ฟใƒผใƒŸใƒŠใƒซใƒ†ใƒผใƒžใ€ใƒ•ใ‚ฉใƒณใƒˆใ€ใใฎไป–ใฎใ‚ณใƒณใƒใƒผใƒใƒณใƒˆใ‚’ๅซใ‚€ใ‚ฟใƒผใƒŸใƒŠใƒซใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บใซๅฏพๅฟœใ—ใฆใ„ใพใ™ใ€‚ +**SSH ใ‚ฟใƒผใƒŸใƒŠใƒซ:** +ใƒ–ใƒฉใ‚ฆใ‚ถใฎใ‚ˆใ†ใชใ‚ฟใƒ–ใจๅˆ†ๅ‰ฒ็”ป้ขใ‚’ๅ‚™ใˆใŸๆœฌๆ ผ็š„ใชใ‚ฟใƒผใƒŸใƒŠใƒซใงใ€ๆœ€ๅคง 6 ๅˆ†ๅ‰ฒใพใงๅŒๆ™‚ใซ่กจ็คบใงใใพใ™ใ€‚ใƒ†ใƒผใƒžใ€ใƒ•ใ‚ฉใƒณใƒˆใ€้…่‰ฒใฏ่‡ช็”ฑใซ้ธในใพใ™ใ€‚ๅ„ใ‚ปใƒƒใ‚ทใƒงใƒณใฎไธŠใฎใƒ„ใƒผใƒซใƒใƒผใซใฏ CPUใ€ใƒกใƒขใƒชใ€ใƒ‡ใ‚ฃใ‚นใ‚ฏใฎ็ŠถๆณใŒใƒชใ‚ขใƒซใ‚ฟใ‚คใƒ ใง่กจ็คบใ•ใ‚Œใ€ใใฎใƒ›ใ‚นใƒˆใฎใƒ•ใ‚กใ‚คใƒซใ€Dockerใ€ใƒˆใƒณใƒใƒซใ€ใƒกใƒˆใƒชใ‚ฏใ‚นใธใ™ใ็งปๅ‹•ใงใใพใ™ใ€‚ -**ใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใ‚ฏใ‚ปใ‚น:** -ใƒ–ใƒฉใ‚ฆใ‚ถไธŠใงRDPใ€VNCใ€Telnetใ‚’ใ‚ตใƒใƒผใƒˆใ€ๅฎŒๅ…จใชใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บใจๅˆ†ๅ‰ฒ็”ป้ขใซๅฏพๅฟœใ—ใฆใ„ใพใ™ใ€‚ +**ใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—:** +RDPใ€VNCใ€Telnet ใ‚’ใƒ–ใƒฉใ‚ฆใ‚ถใ‹ใ‚‰ๅˆฉ็”จใงใใ€ไป–ใฎใ‚ปใƒƒใ‚ทใƒงใƒณใจๅŒใ˜ใ‚ˆใ†ใซใ‚ฟใƒ–ใ‚„ๅˆ†ๅ‰ฒ็”ป้ขใงๆ‰ฑใˆใพใ™ใ€‚RDP ใƒ‰ใƒฉใ‚คใƒ–็”จใฎใƒ•ใ‚กใ‚คใƒซใƒ–ใƒฉใ‚ฆใ‚ถใจใƒ‰ใƒฉใƒƒใ‚ฐ๏ผ†ใƒ‰ใƒญใƒƒใƒ—ใฎใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ‚‚ไฝฟใˆใพใ™ใ€‚Windows ใฎใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—็‰ˆใงใฏใ€ใƒ›ใ‚นใƒˆใ‚’ใƒใ‚คใƒ†ใ‚ฃใƒ–ใฎ RDP ใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆใง้–‹ใใ“ใจใ‚‚ใงใใพใ™ใ€‚ -**SSHใƒˆใƒณใƒใƒซ็ฎก็†:** -่‡ชๅ‹•ๅ†ๆŽฅ็ถšใจใƒ˜ใƒซใ‚นใƒขใƒ‹ใ‚ฟใƒชใƒณใ‚ฐใ€ใƒญใƒผใ‚ซใƒซใƒปใƒชใƒขใƒผใƒˆใƒปใƒ€ใ‚คใƒŠใƒŸใƒƒใ‚ฏSOCKSใƒ•ใ‚ฉใƒฏใƒผใƒ‡ใ‚ฃใƒณใ‚ฐใ‚’ๅ‚™ใˆใŸใ‚ตใƒผใƒใƒผ้–“SSHใƒˆใƒณใƒใƒซใฎไฝœๆˆใƒป็ฎก็†ใŒๅฏ่ƒฝใงใ™ใ€‚ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆๅฏพใ‚ตใƒผใƒใƒผใฎใƒˆใƒณใƒใƒซ่จญๅฎšใฏใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚คใƒณใ‚นใƒˆใƒผใƒซใ”ใจใซใƒญใƒผใ‚ซใƒซใซไฟๅญ˜ใ•ใ‚Œใ€ใ‚ชใƒ—ใ‚ทใƒงใƒณใฎC2Sใƒ—ใƒชใ‚ปใƒƒใƒˆใ‚นใƒŠใƒƒใƒ—ใ‚ทใƒงใƒƒใƒˆใ‚’ใ‚ตใƒผใƒใƒผใซไฟๅญ˜ใƒปๅๅ‰ๅค‰ๆ›ดใƒป่ชญใฟ่พผใฟใƒปๅ‰Š้™คใ—ใฆใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆ้–“ใงใƒญใƒผใ‚ซใƒซใƒˆใƒณใƒใƒซ่จญๅฎšใ‚’็งปๅ‹•ใงใใพใ™ใ€‚ +**SSH ใƒˆใƒณใƒใƒซ:** +ใƒญใƒผใ‚ซใƒซใ€ใƒชใƒขใƒผใƒˆใ€ใƒ€ใ‚คใƒŠใƒŸใƒƒใ‚ฏ SOCKS ใฎ่ปข้€ใซๅฏพๅฟœใ—ใ€่‡ชๅ‹•ๅ†ๆŽฅ็ถšใจใƒ˜ใƒซใ‚นใƒใ‚งใƒƒใ‚ฏใŒไป˜ใ„ใฆใ„ใพใ™ใ€‚ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—็‰ˆใฎใ‚ฏใƒฉใ‚คใ‚ขใƒณใƒˆ้–“ใƒˆใƒณใƒใƒซใฏใใฎ็ซฏๆœซใซไฟๅญ˜ใ•ใ‚Œใ€ใƒ—ใƒชใ‚ปใƒƒใƒˆใ‚’ใ‚ตใƒผใƒใƒผใซไฟๅญ˜ใ—ใฆใŠใ‘ใฐๅˆฅใฎ็ซฏๆœซใซ่จญๅฎšใ‚’็งปใ›ใพใ™ใ€‚ -**ใƒชใƒขใƒผใƒˆใƒ•ใ‚กใ‚คใƒซใƒžใƒใƒผใ‚ธใƒฃใƒผ:** -ใ‚ณใƒผใƒ‰ใ€็”ปๅƒใ€้Ÿณๅฃฐใ€ๅ‹•็”ปใฎ่กจ็คบใƒป็ทจ้›†ใซๅฏพๅฟœใ—ใ€ใƒชใƒขใƒผใƒˆใ‚ตใƒผใƒใƒผไธŠใฎใƒ•ใ‚กใ‚คใƒซใ‚’็›ดๆŽฅ็ฎก็†ใงใใพใ™ใ€‚sudoๅฏพๅฟœใงใƒ•ใ‚กใ‚คใƒซใฎใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ€ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ใ€ๅๅ‰ๅค‰ๆ›ดใ€ๅ‰Š้™คใ€็งปๅ‹•ใ‚’ใ‚ทใƒผใƒ ใƒฌใ‚นใซๅฎŸ่กŒใงใใพใ™ใ€‚ใ‚ตใƒผใƒใƒผ้–“ใงใฎใƒ•ใ‚กใ‚คใƒซ็งปๅ‹•ใซใ‚‚ๅฏพๅฟœใ—ใฆใ„ใพใ™ใ€‚ +**ใƒ•ใ‚กใ‚คใƒซใƒžใƒใƒผใ‚ธใƒฃใƒผ:** +SFTP ใงใƒ•ใ‚กใ‚คใƒซใฎ้–ฒ่ฆงใ€็ทจ้›†ใ€ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ€ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ใ€ๅๅ‰ๅค‰ๆ›ดใ€็งปๅ‹•ใ€ๅ‰Š้™คใŒใงใใ€sudo ใซใ‚‚ๅฏพๅฟœใ—ใฆใ„ใพใ™ใ€‚ใ‚ณใƒผใƒ‰ใ€็”ปๅƒใ€้Ÿณๅฃฐใ€ๅ‹•็”ปใ‚’่กจ็คบใƒป็ทจ้›†ใงใใพใ™ใ€‚ใ‚ตใƒผใƒใƒผ้–“ใง็›ดๆŽฅใƒ•ใ‚กใ‚คใƒซใ‚’ใ‚ณใƒ”ใƒผใงใใ€ๆœ€้€Ÿใฎ็ตŒ่ทฏใŒ่‡ชๅ‹•ใง้ธใฐใ‚Œใ€่ปข้€ใฎๆ•ดๅˆๆ€งใ‚‚ๆคœ่จผใ•ใ‚Œใพใ™ใ€‚ -**DockerใŠใ‚ˆใณPodman็ฎก็†:** -ใ‚ณใƒณใƒ†ใƒŠใฎ่ตทๅ‹•ใ€ๅœๆญขใ€ไธ€ๆ™‚ๅœๆญขใ€ๅ‰Š้™คใ€‚ใ‚ณใƒณใƒ†ใƒŠใฎ็ตฑ่จˆๆƒ…ๅ ฑใ‚’่กจ็คบใ€‚docker execใ‚ฟใƒผใƒŸใƒŠใƒซใงใ‚ณใƒณใƒ†ใƒŠใ‚’ๆ“ไฝœใ€‚DockerใจPodmanใฎไธกๆ–นใ‚’ใ‚ณใƒณใƒ†ใƒŠใƒฉใƒณใ‚ฟใ‚คใƒ ใจใ—ใฆใ‚ตใƒใƒผใƒˆใ—ใฆใ„ใพใ™ใ€‚Portainerใ‚„Dockgeใฎไปฃๆ›ฟใงใฏใชใใ€ใ‚ณใƒณใƒ†ใƒŠใฎไฝœๆˆใ‚ˆใ‚Šใ‚‚็ฐกๆ˜“็š„ใช็ฎก็†ใ‚’็›ฎ็š„ใจใ—ใฆใ„ใพใ™ใ€‚ +**Docker ใจ Podman:** +ใ‚ณใƒณใƒ†ใƒŠใฎ่ตทๅ‹•ใ€ๅœๆญขใ€ไธ€ๆ™‚ๅœๆญขใ€ๅ‰Š้™คใŒใงใใ€็Šถๆ…‹ใ‚’็ขบ่ชใ—ใŸใ‚Šใ€ไธญใงใ‚ทใ‚งใƒซใ‚’้–‹ใ„ใŸใ‚Šใงใใพใ™ใ€‚Docker ใจ Podman ใฎใฉใกใ‚‰ใงใ‚‚ๅ‹•ใใพใ™ใ€‚Portainer ใ‚„ Dockge ใ‚’็ฝฎใๆ›ใˆใ‚‹ใŸใ‚ใฎใ‚‚ใฎใงใฏใชใใ€ใ™ใงใซใ‚ใ‚‹ใ‚ณใƒณใƒ†ใƒŠใ‚’ๆ‰ฑใ†ใŸใ‚ใฎใ‚‚ใฎใงใ™ใ€‚ -**SSHใƒ›ใ‚นใƒˆใƒžใƒใƒผใ‚ธใƒฃใƒผ:** -ใ‚ฟใ‚ฐใ‚„ใƒ•ใ‚ฉใƒซใƒ€๏ผˆใƒ•ใ‚ฉใƒซใƒ€ใฎใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บใจใƒใ‚นใƒˆใƒ•ใ‚ฉใƒซใƒ€ๅฏพๅฟœ๏ผ‰ใงSSHๆŽฅ็ถšใ‚’ไฟๅญ˜ใ€ๆ•ด็†ใ€็ฎก็†ใ—ใ€ๅ†ๅˆฉ็”จๅฏ่ƒฝใชใƒญใ‚ฐใ‚คใƒณๆƒ…ๅ ฑใ‚’็ฐกๅ˜ใซไฟๅญ˜ใ—ใชใŒใ‚‰SSHใ‚ญใƒผใฎใƒ‡ใƒ—ใƒญใ‚คใ‚’่‡ชๅ‹•ๅŒ–ใงใใพใ™ใ€‚ +**ใƒ›ใ‚นใƒˆ็ฎก็†:** +ใ‚ฟใ‚ฐใจใ€ๅๅ‰ใ‚„่‰ฒใ‚’ไป˜ใ‘ใ‚‰ใ‚Œใ‚‹ๅ…ฅใ‚Œๅญใฎใƒ•ใ‚ฉใƒซใƒ€ใงใƒ›ใ‚นใƒˆใ‚’ๆ•ด็†ใงใใพใ™ใ€‚ไฟๅญ˜ใ—ใŸ่ช่จผๆƒ…ๅ ฑใ‚’่ค‡ๆ•ฐใฎใƒ›ใ‚นใƒˆใงไฝฟใ„ๅ›žใ—ใ€SSH ้ตใ‚’่‡ชๅ‹•ใง้…ๅธƒใ—ใ€ใƒ›ใ‚นใƒˆใ‚’่ฆชใƒ›ใ‚นใƒˆใฎไธ‹ใซใพใจใ‚ใ€ไธ€ๆ‹ฌ็ทจ้›†ใ‚„ใ‚จใ‚ฏใ‚นใƒใƒผใƒˆใŒใงใใพใ™ใ€‚ไฟๅญ˜ใ—ใŸใใชใ„ไธ€ๅบฆใใ‚ŠใฎๆŽฅ็ถšใซใฏใ‚ฏใ‚คใƒƒใ‚ฏๆŽฅ็ถšใŒไฝฟใˆใพใ™ใ€‚ @@ -111,83 +111,139 @@ Termixใฏใ€ใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใงๆฐธไน…็„กๆ–™ใฎใ‚ปใƒซใƒ•ใƒ›ใ‚นใƒˆๅž‹ใ‚ชใƒผ **ใƒ›ใ‚นใƒˆใƒกใƒˆใƒชใ‚ฏใ‚น:** -ใปใจใ‚“ใฉใฎLinuxใƒ™ใƒผใ‚นใฎใ‚ตใƒผใƒใƒผใงใ€CPUใ€ใƒกใƒขใƒชใ€ใƒ‡ใ‚ฃใ‚นใ‚ฏไฝฟ็”จ้‡ใ€ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏใ€ใ‚ขใƒƒใƒ—ใ‚ฟใ‚คใƒ ใ€ใ‚ทใ‚นใƒ†ใƒ ๆƒ…ๅ ฑใ€ใƒ•ใ‚กใ‚คใ‚ขใ‚ฆใ‚ฉใƒผใƒซใ€ใƒใƒผใƒˆใƒขใƒ‹ใ‚ฟใƒผใ€ใƒญใ‚ฐใƒ“ใƒฅใƒผใ‚ขใ€ใƒฆใƒผใ‚ถใƒผ/ๆจฉ้™ใ€่จผๆ˜Žๆ›ธใชใฉใ€ใ•ใ‚‰ใซๅคšใใฎๆƒ…ๅ ฑใ‚’่กจ็คบใงใใพใ™ใ€‚ๆ™‚็ณปๅˆ—ใฎๅฑฅๆญดใ‚ฐใƒฉใƒ•ใจใ€ntfyใŠใ‚ˆใณwebhookใซๅฏพๅฟœใ—ใŸใ—ใใ„ๅ€คใƒ™ใƒผใ‚นใฎใ‚ขใƒฉใƒผใƒˆใ‚’ๅซใฟใพใ™ใ€‚ +ใŸใ„ใฆใ„ใฎ Linux ใ‚ตใƒผใƒใƒผใง CPUใ€ใƒกใƒขใƒชใ€ใƒ‡ใ‚ฃใ‚นใ‚ฏใ€ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏใ€ๆธฉๅบฆใ€็จผๅƒๆ™‚้–“ใ€ใƒ—ใƒญใ‚ปใ‚นใ€ใƒใƒผใƒˆใ€ใƒญใ‚ฐใ‚คใƒณใ€ใ‚ทใ‚นใƒ†ใƒ ๆƒ…ๅ ฑใ‚’ๅฑฅๆญดใ‚ฐใƒฉใƒ•ไป˜ใใง็ขบ่ชใงใใพใ™ใ€‚ใƒžใƒใƒผใ‚ธใƒฃใƒผใ‚ซใƒผใƒ‰ใ‚’ไฝฟใˆใฐใ€ใ‚ตใƒผใƒ“ใ‚นใ€cronใ€ใƒ‘ใƒƒใ‚ฑใƒผใ‚ธใ€ใƒฆใƒผใ‚ถใƒผใ€ใƒ•ใ‚กใ‚คใ‚ขใ‚ฆใ‚ฉใƒผใƒซใ€WireGuardใ€Tailscaleใ€SSL ่จผๆ˜Žๆ›ธใ€ใƒญใ‚ฐใ€ใƒ˜ใƒซใ‚นใƒใ‚งใƒƒใ‚ฏใ‚’ Termix ใ‹ใ‚‰้›ขใ‚Œใšใซๆ‰ฑใˆใพใ™ใ€‚ -**ใƒฆใƒผใ‚ถใƒผ่ช่จผ:** -็ฎก็†่€…ใ‚ณใƒณใƒˆใƒญใƒผใƒซ๏ผˆไป–ใฎใƒฆใƒผใ‚ถใƒผๆƒ…ๅ ฑใ‚’็ทจ้›†ๅฏ่ƒฝ๏ผ‰ใจOIDC/LDAP/SSO๏ผˆใ‚ขใ‚ฏใ‚ปใ‚นๅˆถๅพกไป˜ใ๏ผ‰ใ€2FA๏ผˆTOTP๏ผ‰ใ€ใƒ‘ใ‚นใ‚ญใƒผ๏ผˆWebAuthn๏ผ‰ๅฏพๅฟœใซใ‚ˆใ‚‹ๅฎ‰ๅ…จใชใƒฆใƒผใ‚ถใƒผ็ฎก็†ใ€‚ใ™ในใฆใฎใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ใงใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ–ใชใƒฆใƒผใ‚ถใƒผใ‚ปใƒƒใ‚ทใƒงใƒณใ‚’่กจ็คบใ—ใ€ๆจฉ้™ใ‚’ๅ–ใ‚Šๆถˆใ—ๅฏ่ƒฝใ€‚OIDC/ใƒญใƒผใ‚ซใƒซใ‚ขใ‚ซใ‚ฆใƒณใƒˆใฎ้€ฃๆบใŒๅฏ่ƒฝใงใ™ใ€‚ใ™ในใฆใฎใƒฆใƒผใ‚ถใƒผๆ“ไฝœใฎ็›ฃๆŸปใƒญใ‚ฐใ‚’่กจ็คบใงใใพใ™ใ€‚ +**่‡ชๅ‹•ๅŒ–:** +ใใฃใ‹ใ‘ใ‚’้ธใ‚“ใงใ€ไฝ•ใ‚’ใ™ใ‚‹ใ‹ใ‚’ๆฑบใ‚ใ‚‹ใ ใ‘ใงใ™ใ€‚ใใฃใ‹ใ‘ใซใฏใ€ใƒกใƒˆใƒชใ‚ฏใ‚นใŒใ—ใใ„ๅ€คใ‚’่ถ…ใˆใŸใจใใ€ใƒ›ใ‚นใƒˆใŒไธŠใŒใฃใŸใ‚Š่ฝใกใŸใ‚Šใ—ใŸใจใใ€ใƒ˜ใƒซใ‚นใƒใ‚งใƒƒใ‚ฏใฎ็Šถๆ…‹ใŒๅค‰ใ‚ใฃใŸใจใใ€ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซใ€ใ‚ณใƒณใƒ†ใƒŠใฎใ‚คใƒ™ใƒณใƒˆใ€ๅค–้ƒจใ‹ใ‚‰ใฎ Webhook ใŒใ‚ใ‚Šใพใ™ใ€‚ใ‚นใƒ†ใƒƒใƒ—ใงใฏใ‚ณใƒžใƒณใƒ‰ใ‚„ใ‚นใƒ‹ใƒšใƒƒใƒˆใฎๅฎŸ่กŒใ€ใ‚ณใƒณใƒ†ใƒŠใ‚„ใƒˆใƒณใƒใƒซใฎๆ“ไฝœใ€ใƒ›ใ‚นใƒˆใฎ่ตทๅ‹•ใ€URL ใฎๅ‘ผใณๅ‡บใ—ใ€ๅพ…ๆฉŸใ€ๆกไปถๅˆ†ๅฒใ€ๅˆฅใฎ่‡ชๅ‹•ๅŒ–ใฎๅฎŸ่กŒใŒใงใใ€ntfyใ€Discordใ€Webhook ใง้€š็Ÿฅใงใใพใ™ใ€‚ใƒ†ใ‚นใƒˆๅฎŸ่กŒใงๅฎ‰ๅ…จใซ่ฉฆใ›ใพใ™ใ€‚ -**Tailscaleใ‚คใƒณใƒ†ใ‚ฐใƒฌใƒผใ‚ทใƒงใƒณ:** -Tailnetใฎใƒ‡ใƒใ‚คใ‚นใ‚’ใƒชใ‚นใƒˆใ—ใฆใƒ›ใ‚นใƒˆใจใ—ใฆใ™ใฐใ‚„ใ่ฟฝๅŠ ใ—ใ€Tailscale SSHใ‚’่ช่จผๆ–นๆณ•ใจใ—ใฆไฝฟ็”จใ—ใฆๆŽฅ็ถšใ—ใพใ™ใ€‚ใ“ใ‚Œใซใ‚ˆใ‚Šใ€TailnetใฎACLใŒ่ช่จผๆƒ…ๅ ฑใ‚’ไฟๅญ˜ใ›ใšใซ่ชๅฏใ‚’ๅ‡ฆ็†ใ—ใพใ™ใ€‚ +**ใƒ•ใƒชใƒผใƒˆ:** +ใƒ›ใ‚นใƒˆใ‚’้ธใถใ‹ใ€ใ‚ฟใ‚ฐใฎใƒซใƒผใƒซใ‚’ๆฑบใ‚ใฆใƒ•ใƒชใƒผใƒˆใซใพใจใ‚ใ‚‹ใจใ€ๆ–ฐใ—ใ„ใƒ›ใ‚นใƒˆใฏ่‡ชๅ‹•ใงๅ…ฅใ‚Šใพใ™ใ€‚ใ™ในใฆใฎใƒ›ใ‚นใƒˆใงๅŒใ˜ใ‚ณใƒžใƒณใƒ‰ใ‚’ไธ€ๅบฆใซๅฎŸ่กŒใ—ใ€ๅ…จๅฐใซใƒ•ใ‚กใ‚คใƒซใ‚’้…ใฃใŸใ‚Š้›†ใ‚ใŸใ‚Šใ€ใƒ‘ใƒƒใ‚ฑใƒผใ‚ธใ‚’ๅ…ฅใ‚ŒใŸใ‚Šใ€OSใ€ใ‚ซใƒผใƒใƒซใ€ใ‚ขใƒผใ‚ญใƒ†ใ‚ฏใƒใƒฃใ€็จผๅƒๆ™‚้–“ใฎไธ€่ฆงใ‚’้›†ใ‚ใ‚‰ใ‚Œใพใ™ใ€‚ -**RBAC/ๅ…ฑๆœ‰:** -ใƒญใƒผใƒซใ‚’ไฝœๆˆใ—ใ€ใƒฆใƒผใ‚ถใƒผ/ใƒญใƒผใƒซ้–“ใงใƒ›ใ‚นใƒˆใ‚’ๅ…ฑๆœ‰ใงใใพใ™ใ€‚ใ™ในใฆใฎ่ช่จผใ‚ฟใ‚คใƒ—ใจใ™ในใฆใฎใƒ›ใ‚นใƒˆใƒ—ใƒญใƒˆใ‚ณใƒซใซๅฏพๅฟœใ—ใฆใ„ใพใ™ใ€‚ +**AI ใ‚ขใ‚ทใ‚นใ‚ฟใƒณใƒˆ:** +ไปปๆ„ใฎๆฉŸ่ƒฝใงใ€่‡ชๅˆ†ใงๆœ‰ๅŠนใซใ™ใ‚‹ใพใงใฏๅ‹•ใใพใ›ใ‚“ใ€‚OpenAIใ€Anthropicใ€Geminiใ€Ollamaใ€ใพใŸใฏ OpenAI ไบ’ๆ›ใฎใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆใซใคใชใ„ใงใ€่‡ชๅˆ†ใฎ็’ฐๅขƒใซใคใ„ใฆ่ณชๅ•ใงใใพใ™ใ€‚ใƒ›ใ‚นใƒˆใ€ใƒ•ใƒชใƒผใƒˆใ€ใ‚นใƒ‹ใƒšใƒƒใƒˆใ€ใ‚ขใƒฉใƒผใƒˆใ‚’่ชญใฟๅ–ใ‚Œใพใ™ใŒใ€ๅค‰ๆ›ดใฏ่‡ชๅˆ†ใง่กŒใ‚ใšใ€ๆ‰ฟ่ชใ—ใฆใ‚‚ใ‚‰ใ†ใŸใ‚ใฎๆๆกˆใจใ—ใฆๅ‡บใ—ใพใ™ใ€‚่ช่จผๆƒ…ๅ ฑใ€ใƒฆใƒผใ‚ถใƒผใ€่จญๅฎšใซใฏๆฑบใ—ใฆ่งฆใ‚Œใ‚‰ใ‚Œใพใ›ใ‚“ใ€‚็ฎก็†่€…ใฏใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นๅ…จไฝ“ใง็„กๅŠนใซใงใใ€ๅˆๆœŸ่จญๅฎšใง้ž่กจ็คบใซใ™ใ‚‹ใ“ใจใ‚‚ใงใใพใ™ใ€‚ -**ใ‚ทใƒชใ‚ขใƒซๆŽฅ็ถš:** -ใƒ–ใƒฉใ‚ฆใ‚ถใพใŸใฏใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใƒ—ใƒชใ‹ใ‚‰ใ‚ทใƒชใ‚ขใƒซใƒ‡ใƒใ‚คใ‚น๏ผˆใƒซใƒผใ‚ฟใƒผใ€ใ‚นใ‚คใƒƒใƒใ€ใƒžใ‚คใ‚ฏใƒญใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใชใฉ๏ผ‰ใซ็›ดๆŽฅๆŽฅ็ถšใงใใพใ™ใ€‚ใƒœใƒผใƒฌใƒผใƒˆใ€ใƒ‡ใƒผใ‚ฟใƒ“ใƒƒใƒˆใ€ใ‚นใƒˆใƒƒใƒ—ใƒ“ใƒƒใƒˆใ€ใƒ‘ใƒชใƒ†ใ‚ฃใ‚’่จญๅฎšใงใใพใ™ใ€‚ๅฏพๅฟœใƒ–ใƒฉใ‚ฆใ‚ถใงใฏWeb Serial APIใ‚’ไฝฟ็”จใ—ใ€Electronใ‚ขใƒ—ใƒชใงใฏใƒใ‚คใƒ†ใ‚ฃใƒ–ใƒใƒƒใ‚ฏใ‚จใƒณใƒ‰ใ‚’ไฝฟ็”จใ—ใพใ™ใ€‚ +**ใƒญใ‚ฐใ‚คใƒณใจใƒฆใƒผใ‚ถใƒผ:** +ใƒญใƒผใ‚ซใƒซใ‚ขใ‚ซใ‚ฆใƒณใƒˆใซๅŠ ใˆใฆ OIDCใ€LDAPใ€GitHubใ€Google ใงใฎใ‚ตใ‚คใƒณใ‚คใƒณใซๅฏพๅฟœใ—ใ€2 ่ฆ็ด ่ช่จผ๏ผˆTOTP๏ผ‰ใ€ใƒ‘ใ‚นใ‚ญใƒผ๏ผˆWebAuthn๏ผ‰ใ€ไฟก้ ผๆธˆใฟใƒ‡ใƒใ‚คใ‚นใ‚‚ไฝฟใˆใพใ™ใ€‚็ฎก็†่€…ใฏใƒฆใƒผใ‚ถใƒผใฎ็ฎก็†ใ€OIDC ใ‚ฐใƒซใƒผใƒ—ใจใƒญใƒผใƒซใฎๅฏพๅฟœไป˜ใ‘ใ€ๅ…จใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ใฎใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ–ใชใ‚ปใƒƒใ‚ทใƒงใƒณใฎ็ขบ่ชใจๅคฑๅŠนใŒใงใใพใ™ใ€‚ใƒญใƒผใ‚ซใƒซใจ OIDC ใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’้€ฃๆบใงใใ€่ชฐใŒไฝ•ใ‚’ใ—ใŸใ‹ใฏ็›ฃๆŸปใƒญใ‚ฐใง็ขบ่ชใงใใพใ™ใ€‚ +**ใƒญใƒผใƒซใจๅ…ฑๆœ‰:** +ใƒญใƒผใƒซใ‚’ไฝœใ‚Šใ€ๆŽฅ็ถšใ€้–ฒ่ฆงใ€็ทจ้›†ใ€็ฎก็†ใจใ„ใ† 4 ๆฎต้šŽใงใƒ›ใ‚นใƒˆใ‚’ใƒฆใƒผใ‚ถใƒผใ‚„ใƒญใƒผใƒซใซๅ…ฑๆœ‰ใงใใพใ™ใ€‚ใ™ในใฆใฎ่ช่จผๆ–นๅผใจใ™ในใฆใฎใƒ—ใƒญใƒˆใ‚ณใƒซใงไฝฟใˆใ€ๅ…ฑๆœ‰ใ—ใŸใƒ›ใ‚นใƒˆใงไฝฟใ†่ช่จผๆƒ…ๅ ฑใ‚’ไธŠๆ›ธใใ™ใ‚‹ใ“ใจใ‚‚ใงใใพใ™ใ€‚ + + + + + + **ใ‚ขใƒฉใƒผใƒˆ:** -ใƒ›ใ‚นใƒˆใƒกใƒˆใƒชใ‚ฏใ‚น๏ผˆCPUใ€ใƒกใƒขใƒชใ€ใƒ‡ใ‚ฃใ‚นใ‚ฏใชใฉ๏ผ‰ใซๅฏพใ—ใฆใ—ใใ„ๅ€คใƒ™ใƒผใ‚นใฎใ‚ขใƒฉใƒผใƒˆใƒซใƒผใƒซใ‚’่จญๅฎšใ—ใ€็™บๅ‹•ๆ™‚ใซntfyใพใŸใฏwebhookใง้€š็Ÿฅใ‚’ๅ—ใ‘ๅ–ใ‚Œใพใ™ใ€‚็™บๅ‹•ไธญใŠใ‚ˆใณ่งฃๆฑบๆธˆใฟใฎใ‚ขใƒฉใƒผใƒˆใ‚’ๅฑฅๆญดใƒญใ‚ฐใง็ขบ่ชใงใใพใ™ใ€‚ +CPUใ€ใƒกใƒขใƒชใ€ใƒ‡ใ‚ฃใ‚นใ‚ฏใชใฉใฎใƒ›ใ‚นใƒˆใƒกใƒˆใƒชใ‚ฏใ‚นใซใƒซใƒผใƒซใ‚’่จญๅฎšใ—ใ€็™บๅ ฑใ—ใŸใ‚‰ ntfyใ€Discordใ€Webhook ใง้€š็Ÿฅใ‚’ๅ—ใ‘ๅ–ใ‚Œใพใ™ใ€‚็™บๅ ฑไธญใจ่งฃๆถˆๆธˆใฟใฎใ‚ขใƒฉใƒผใƒˆใฏๅฑฅๆญดใง็ขบ่ชใงใใ€ๆฐ—ใซใ—ใชใ„ใ‚‚ใฎใฏๆถˆใ—ใฆใŠใ‘ใพใ™ใ€‚ - - **ใƒ›ใƒผใƒ ใƒšใƒผใ‚ธ:** -ใƒ‰ใƒฉใƒƒใ‚ฐ๏ผ†ใƒ‰ใƒญใƒƒใƒ—ใฎใ‚ฆใ‚ฃใ‚ธใ‚งใƒƒใƒˆใ‚ฐใƒชใƒƒใƒ‰ใ‚’ๅ‚™ใˆใŸๅฎŒๅ…จใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บๅฏ่ƒฝใชใƒ›ใƒผใƒ ใƒšใƒผใ‚ธใ€‚ใƒ›ใ‚นใƒˆใ‚นใƒ†ใƒผใ‚ฟใ‚นใ€ใ‚ตใƒผใƒ“ใ‚นใƒชใƒณใ‚ฏใ€ๆ™‚่จˆใ€ใƒกใƒขใ€RSSใƒ•ใ‚ฃใƒผใƒ‰ใ€ๅคฉๆฐ—ใ€Dockerใ‚ณใƒณใƒ†ใƒŠใ€ใƒ›ใ‚นใƒˆใƒกใƒˆใƒชใ‚ฏใ‚นใ‚ฐใƒฉใƒ•ใ€ๅŸ‹ใ‚่พผใฟใ‚ฟใƒผใƒŸใƒŠใƒซใ€iframeใชใฉใฎใ‚ฆใ‚ฃใ‚ธใ‚งใƒƒใƒˆใ‚’่ฟฝๅŠ ใงใใพใ™ใ€‚ - - - - -**ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นๆš—ๅทๅŒ–:** -ใƒใƒƒใ‚ฏใ‚จใƒณใƒ‰ใฏๆš—ๅทๅŒ–ใ•ใ‚ŒใŸSQLiteใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นใƒ•ใ‚กใ‚คใƒซใจใ—ใฆไฟๅญ˜ใ•ใ‚Œใพใ™ใ€‚่ฉณ็ดฐใฏ[ใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ](https://docs.termix.site/security)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ +่‡ชๅˆ†ใง็ต„ใฟ็ซ‹ใฆใ‚‹ใƒ‰ใƒฉใƒƒใ‚ฐ๏ผ†ใƒ‰ใƒญใƒƒใƒ—ใฎใ‚ฆใ‚ฃใ‚ธใ‚งใƒƒใƒˆ็”ป้ขใงใ™ใ€‚ใƒ›ใ‚นใƒˆใฎ็Šถๆ…‹ใ€Pingใ€ใ‚ตใƒผใƒ“ใ‚นใƒชใƒณใ‚ฏใ€ใƒ–ใƒƒใ‚ฏใƒžใƒผใ‚ฏใ€ๆคœ็ดขใ€ๆ™‚่จˆใ€ใ‚ซใƒฌใƒณใƒ€ใƒผใ€ใ‚ซใ‚ฆใƒณใƒˆใƒ€ใ‚ฆใƒณใ€ใƒกใƒขใ€RSSใ€ๅคฉๆฐ—ใ€็”ปๅƒใ€iframeใ€Dockerใ€ใƒˆใƒณใƒใƒซใ€ใƒกใƒˆใƒชใ‚ฏใ‚นใฎใ‚ฐใƒฉใƒ•ใ€็‹ฌ่‡ช APIใ€ใ•ใ‚‰ใซใฏใƒฉใ‚คใƒ–ใฎใ‚ฟใƒผใƒŸใƒŠใƒซใพใงใ‚ฆใ‚ฃใ‚ธใ‚งใƒƒใƒˆใจใ—ใฆ็ฝฎใ‘ใพใ™ใ€‚ -**ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏใ‚ฐใƒฉใƒ•:** -ใƒ€ใƒƒใ‚ทใƒฅใƒœใƒผใƒ‰ใ‚’ใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บใ—ใฆใ€SSHๆŽฅ็ถšใซๅŸบใฅใใƒ›ใƒผใƒ ใƒฉใƒœใฎใƒใƒƒใƒˆใƒฏใƒผใ‚ฏใ‚’ใ‚นใƒ†ใƒผใ‚ฟใ‚น่กจ็คบไป˜ใใงๅฏ่ฆ–ๅŒ–ใงใใพใ™ใ€‚ +**ใ‚นใƒ‹ใƒšใƒƒใƒˆใจใƒ„ใƒผใƒซ:** +ใ‚ˆใไฝฟใ†ใ‚ณใƒžใƒณใƒ‰ใ‚’ไฟๅญ˜ใ—ใฆใ€ใƒฏใƒณใ‚ฏใƒชใƒƒใ‚ฏใงๅฎŸ่กŒใงใใพใ™ใ€‚ใƒ›ใ‚นใƒˆใฎๅ€คใ‚„่‡ชๅˆ†ใงๅ…ฅๅŠ›ใ™ใ‚‹ๅ€คใ‚’ๅค‰ๆ•ฐใจใ—ใฆไฝฟใˆใพใ™ใ€‚้–‹ใ„ใฆใ„ใ‚‹ใ™ในใฆใฎใ‚ฟใƒผใƒŸใƒŠใƒซใงๅŒใ˜ใ‚ณใƒžใƒณใƒ‰ใ‚’ใพใจใ‚ใฆๅฎŸ่กŒใงใใ€ใ‚ณใƒžใƒณใƒ‰ๅฑฅๆญดใ‚‚่ฃœๅฎŒไป˜ใใงๆคœ็ดขใงใใพใ™ใ€‚ -**SSHใƒ„ใƒผใƒซ:** -ใƒฏใƒณใ‚ฏใƒชใƒƒใ‚ฏใงๅฎŸ่กŒใงใใ‚‹ๅ†ๅˆฉ็”จๅฏ่ƒฝใชใ‚ณใƒžใƒณใƒ‰ใ‚นใƒ‹ใƒšใƒƒใƒˆใฎไฝœๆˆใ€‚่ค‡ๆ•ฐใฎ้–‹ใ„ใฆใ„ใ‚‹ใ‚ฟใƒผใƒŸใƒŠใƒซใซๅฏพใ—ใฆๅŒๆ™‚ใซใ‚ณใƒžใƒณใƒ‰ใ‚’ๅฎŸ่กŒใงใใพใ™ใ€‚ +**ใ‚ปใƒƒใ‚ทใƒงใƒณๅ…ฑๆœ‰:** +ใ‚ฟใƒผใƒŸใƒŠใƒซใ€RDPใ€VNCใ€Telnet ใฎใ‚ปใƒƒใ‚ทใƒงใƒณใ‚’ใƒชใ‚ขใƒซใ‚ฟใ‚คใƒ ใงๅ…ฑๆœ‰ใงใใพใ™ใ€‚ใ‚ขใ‚ซใ‚ฆใƒณใƒˆใชใ—ใงๅ‚ๅŠ ใงใใ‚‹ใƒชใƒณใ‚ฏใ‚’้€ใ‚‹ใ‹ใ€็‰นๅฎšใฎ Termix ใƒฆใƒผใ‚ถใƒผใจๅ…ฑๆœ‰ใ—ใ€้–ฒ่ฆงใฎใฟใ‹ๆ“ไฝœๅฏ่ƒฝใ‹ใ‚’้ธในใพใ™ใ€‚ๅ…ฑๆœ‰ใฏ่‡ชๅ‹•ใงๆœŸ้™ๅˆ‡ใ‚Œใซใ‚‚ใ€ใ„ใคใงใ‚‚ๅ–ใ‚Šๆถˆใ—ใซใ‚‚ใงใใ€ๅ…จไฝ“ใพใŸใฏใƒ›ใ‚นใƒˆใ”ใจใซใ‚ชใƒ•ใซใงใใพใ™ใ€‚ -**ๆฐธ็ถšใ‚ฟใƒ–:** -ใƒฆใƒผใ‚ถใƒผใƒ—ใƒญใƒ•ใ‚ฃใƒผใƒซใงๆœ‰ๅŠนใซใ™ใ‚‹ใจใ€SSHใ‚ปใƒƒใ‚ทใƒงใƒณใจใ‚ฟใƒ–ใŒใƒ‡ใƒใ‚คใ‚น/ๆ›ดๆ–ฐใ‚’ใพใŸใ„ใง้–‹ใ„ใŸใพใพไฟๆŒใ•ใ‚Œใพใ™ใ€‚ +**ใ‚ปใƒƒใ‚ทใƒงใƒณ้Œฒ็”ปใจใƒญใ‚ฐ:** +ใ‚ฟใƒผใƒŸใƒŠใƒซใ€RDPใ€VNC ใฎใ‚ปใƒƒใ‚ทใƒงใƒณใ‚’้Œฒ็”ปใ—ใฆใ€ใ‚ใจใ‹ใ‚‰ๅ†็”Ÿใงใใพใ™ใ€‚ใ‚ปใƒƒใ‚ทใƒงใƒณใฎใƒ†ใ‚ญใ‚นใƒˆใƒญใ‚ฐใ‚’ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ใงใใ€ๆŽฅ็ถšใƒญใ‚ฐใ‚’่ฆ‹ใ‚ŒใฐๆŽฅ็ถšไธญใซไฝ•ใŒ่ตทใใŸใ‹ใŒใใฎใพใพๅˆ†ใ‹ใ‚Šใพใ™ใ€‚ + + + + +**ใ‚ทใƒชใ‚ขใƒซๆŽฅ็ถš:** +ใƒซใƒผใ‚ฟใƒผใ€ใ‚นใ‚คใƒƒใƒใ€ใƒžใ‚คใ‚ณใƒณใชใฉใฎใ‚ทใƒชใ‚ขใƒซๆฉŸๅ™จใซใ€ใƒ–ใƒฉใ‚ฆใ‚ถใ‚„ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใƒ—ใƒชใ‹ใ‚‰ๆŽฅ็ถšใงใใพใ™ใ€‚ใƒœใƒผใƒฌใƒผใƒˆใ€ใƒ‡ใƒผใ‚ฟใƒ“ใƒƒใƒˆใ€ใ‚นใƒˆใƒƒใƒ—ใƒ“ใƒƒใƒˆใ€ใƒ‘ใƒชใƒ†ใ‚ฃใ‚’่จญๅฎšใงใใพใ™ใ€‚ๅฏพๅฟœใƒ–ใƒฉใ‚ฆใ‚ถใงใฏ Web Serial API ใ‚’ใ€ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใƒ—ใƒชใงใฏใƒใ‚คใƒ†ใ‚ฃใƒ–ใฎใƒใƒƒใ‚ฏใ‚จใƒณใƒ‰ใ‚’ไฝฟใ„ใพใ™ใ€‚ + + + + + + +**Tailscale:** +tailnet ใ‹ใ‚‰็ซฏๆœซใ‚’ๅ–ใ‚Š่พผใ‚“ใงๆ•ฐใ‚ฏใƒชใƒƒใ‚ฏใงใƒ›ใ‚นใƒˆใจใ—ใฆ่ฟฝๅŠ ใงใใ€Tailscale SSH ใงๆŽฅ็ถšใ™ใ‚Œใฐใ‚ขใ‚ฏใ‚ปใ‚นๅˆถๅพกใฏ tailnet ใฎ ACL ใซไปปใ›ใ‚‰ใ‚Œใ€่ช่จผๆƒ…ๅ ฑใ‚’ไฟๅญ˜ใ™ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚Šใพใ›ใ‚“ใ€‚Headscale ใ‚„็‹ฌ่‡ชใฎใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆใซใ‚‚ๅฏพๅฟœใ—ใฆใ„ใพใ™ใ€‚ + + + + +**Proxmox:** +Proxmox ใฎใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‹ใ‚‰ใใฎใพใพใƒ›ใ‚นใƒˆใ‚’ๅ–ใ‚Š่พผใ‚ใพใ™ใ€‚ใƒŽใƒผใƒ‰ใ‚„ใ‚ฒใ‚นใƒˆใฎ CPUใ€ใƒกใƒขใƒชใ€ใ‚นใƒˆใƒฌใƒผใ‚ธใชใฉใฎ็Šถๆ…‹ใ‚’ๅฐ‚็”จใฎใ‚ฟใƒ–ใง็ขบ่ชใงใใพใ™ใ€‚ + + + + + + +**ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใจใ‚ฟใƒ–:** +ใ‚ฟใƒ–ใจๅˆ†ๅ‰ฒใƒฌใ‚คใ‚ขใ‚ฆใƒˆใฎใ‚ปใƒƒใƒˆใ‚’ไฟๅญ˜ใ—ใฆใ€ใƒฏใƒณใ‚ฏใƒชใƒƒใ‚ฏใงใพใ‚‹ใ”ใจ้–‹ใ็›ดใ›ใพใ™ใ€‚Termix ใฏๅ‰ๅ›žใฎใ‚ปใƒƒใ‚ทใƒงใƒณใ‚‚่ฆšใˆใฆใ„ใ‚‹ใฎใงใ€ๅ†่ชญใฟ่พผใฟใ—ใฆใ‚‚็ซฏๆœซใ‚’ๅค‰ใˆใฆใ‚‚ใ‚ฟใƒ–ใฏๆˆปใฃใฆใใพใ™ใ€‚ + + + + +**ใ‚ฌใ‚คใƒ‰ไป˜ใใ‚ปใƒƒใƒˆใ‚ขใƒƒใƒ—:** +็Ÿญใ„ใ‚ปใƒƒใƒˆใ‚ขใƒƒใƒ—ใŒใ€็”ป้ขใฎใƒ—ใƒชใ‚ปใƒƒใƒˆใ€ใƒ†ใƒผใƒžใ€ไฝฟใ„ใŸใ„ๆฉŸ่ƒฝใ€ๆœ€ๅˆใฎใƒ›ใ‚นใƒˆใฎ้ธๆŠžใ‚’ๆกˆๅ†…ใ—ใพใ™ใ€‚ใ‚ทใƒณใƒ—ใƒซใƒขใƒผใƒ‰ใฏไฝฟใ‚ใชใ„ใ‚‚ใฎใ‚’้š ใ—ใฆใใ‚Œใพใ™ใ€‚ใ‚ปใƒƒใƒˆใ‚ขใƒƒใƒ—ใฏใ„ใคใงใ‚‚ใ‚„ใ‚Š็›ดใ›ใพใ™ใ—ใ€ใƒ—ใƒชใ‚ปใƒƒใƒˆใ‚‚ๅˆ‡ใ‚Šๆ›ฟใˆใ‚‰ใ‚Œใพใ™ใ€‚ + + + + + + +**ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ๅ˜ไฝ“ๅˆฉ็”จใจๅŒๆœŸ:** +ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใƒ—ใƒชใฏใƒญใƒผใ‚ซใƒซใฎใƒใƒƒใ‚ฏใ‚จใƒณใƒ‰ใจใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นใ‚’ๆŒใกใ€ใ‚ตใƒผใƒใƒผใชใ—ใงๅ˜ไฝ“ใงๅ‹•ใใพใ™ใ€‚Termix ใ‚ตใƒผใƒใƒผใซใคใชใ’ใฐใ€ใƒ›ใ‚นใƒˆใ€่ช่จผๆƒ…ๅ ฑใ€ใ‚นใƒ‹ใƒšใƒƒใƒˆใชใฉใ‚’ๅŒๆ–นๅ‘ใงๅŒๆœŸใงใใ€ๆŽฅ็ถšใ‚’ใƒญใƒผใ‚ซใƒซใ‹ใ‚‰ๅง‹ใ‚ใ‚‹ใ‹ใ‚ตใƒผใƒใƒผ็ตŒ็”ฑใซใ™ใ‚‹ใ‹ใ‚‚้ธในใพใ™ใ€‚ + + + + +**ใ‚ณใƒžใƒณใƒ‰ใƒฉใ‚คใƒณใƒ„ใƒผใƒซ:** +ใ‚ทใ‚งใƒซใ‚„ใ‚นใ‚ฏใƒชใƒ—ใƒˆใ‹ใ‚‰ไฝฟใˆใ‚‹ `termix` CLI ใงใ™ใ€‚ใ‚ฟใƒผใƒŸใƒŠใƒซใ‚’้–‹ใใ€ใƒ›ใ‚นใƒˆ 1 ๅฐใพใŸใฏใƒ•ใƒชใƒผใƒˆๅ…จไฝ“ใงใ‚ณใƒžใƒณใƒ‰ใ‚’ๅฎŸ่กŒใ—ใ€SFTP ใงใƒ•ใ‚กใ‚คใƒซใ‚’็งปๅ‹•ใ—ใ€ใƒ›ใ‚นใƒˆใ‚„ใ‚นใƒ‹ใƒšใƒƒใƒˆใ€่ช่จผๆƒ…ๅ ฑใ‚’็ฎก็†ใงใใพใ™ใ€‚`npm install -g @termix-cli/cli` ใงๅ…ฅใ‚Œใ‚‹ใ‹ใ€ๅ˜ไฝ“ใฎใƒใ‚คใƒŠใƒชใ‚’ไฝฟใฃใฆใใ ใ•ใ„ใ€‚่ฉณใ—ใใฏ [CLI ใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ](https://docs.termix.site/cli)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ + + + + + + +**ใ‚ปใ‚ญใƒฅใƒชใƒ†ใ‚ฃ:** +ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚„้ตใชใฉใฎ็ง˜ๅฏ†ๆƒ…ๅ ฑใฏใƒฆใƒผใ‚ถใƒผใ”ใจใซๆš—ๅทๅŒ–ใ•ใ‚Œใ€ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นใฎใƒ•ใ‚กใ‚คใƒซ่‡ชไฝ“ใ‚‚ใƒ‡ใ‚ฃใ‚นใ‚ฏไธŠใงๆš—ๅทๅŒ–ใงใใพใ™ใ€‚ไป•็ต„ใฟใฏ[ใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ](https://docs.termix.site/security)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ **ๅคš่จ€่ชžๅฏพๅฟœ:** -็ด„30่จ€่ชžใฎ็ต„ใฟ่พผใฟใ‚ตใƒใƒผใƒˆ๏ผˆ[Crowdin](https://docs.termix.site/translations)ใง็ฎก็†ใ•ใ‚Œใฆใ„ใพใ™๏ผ‰ใ€‚ +็ด„ 30 ่จ€่ชžใซๅฏพๅฟœใ—ใฆใŠใ‚Šใ€[Crowdin](https://docs.termix.site/translations) ใง็ฎก็†ใ—ใฆใ„ใพใ™ใ€‚ @@ -199,17 +255,20 @@ Tailnetใฎใƒ‡ใƒใ‚คใ‚นใ‚’ใƒชใ‚นใƒˆใ—ใฆใƒ›ใ‚นใƒˆใจใ—ใฆใ™ใฐใ‚„ใ่ฟฝๅŠ ใ— ใใฎไป–ใฎๆฉŸ่ƒฝ
-- **ใƒ€ใƒƒใ‚ทใƒฅใƒœใƒผใƒ‰** - ใƒ€ใƒƒใ‚ทใƒฅใƒœใƒผใƒ‰ใงใ‚ตใƒผใƒใƒผๆƒ…ๅ ฑใ‚’ไธ€็›ฎใง็ขบ่ชใงใใพใ™ -- **APIใ‚ญใƒผ** - ่‡ชๅ‹•ๅŒ–/CI็”จใซๆœ‰ๅŠนๆœŸ้™ไป˜ใใฎใƒฆใƒผใ‚ถใƒผใ‚นใ‚ณใƒผใƒ—APIใ‚ญใƒผใ‚’ไฝœๆˆใงใใพใ™ -- **ใƒ‡ใƒผใ‚ฟใฎใ‚จใ‚ฏใ‚นใƒใƒผใƒˆ/ใ‚คใƒณใƒใƒผใƒˆ** - SSHใƒ›ใ‚นใƒˆใ€่ช่จผๆƒ…ๅ ฑใ€ใƒ•ใ‚กใ‚คใƒซใƒžใƒใƒผใ‚ธใƒฃใƒผใƒ‡ใƒผใ‚ฟใฎใ‚จใ‚ฏใ‚นใƒใƒผใƒˆใจใ‚คใƒณใƒใƒผใƒˆใŒๅฏ่ƒฝใงใ™ -- **่‡ชๅ‹•SSL่จญๅฎš** - HTTPSใƒชใƒ€ใ‚คใƒฌใ‚ฏใƒˆไป˜ใใฎ็ต„ใฟ่พผใฟSSL่จผๆ˜Žๆ›ธ็”Ÿๆˆใƒป็ฎก็†ใŒๅฏ่ƒฝใงใ™ -- **ใƒขใƒ€ใƒณUI** - Reactใ€Tailwind CSSใ€Shadcnใงๆง‹็ฏ‰ใ•ใ‚ŒใŸใ€ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—/ใƒขใƒใ‚คใƒซๅฏพๅฟœใฎใ‚ฏใƒชใƒผใƒณใชใ‚คใƒณใ‚ฟใƒผใƒ•ใ‚งใƒผใ‚นใ€‚ใƒฉใ‚คใƒˆใ€ใƒ€ใƒผใ‚ฏใ€Draculaใชใฉใ€ๅคšใใฎ็•ฐใชใ‚‹UIใƒ†ใƒผใƒžใ‹ใ‚‰้ธๆŠžๅฏ่ƒฝใ€‚URLใƒซใƒผใƒˆใงไปปๆ„ใฎๆŽฅ็ถšใ‚’ใƒ•ใƒซใ‚นใ‚ฏใƒชใƒผใƒณใง้–‹ใใ“ใจใŒใงใใพใ™ใ€‚ -- **ใ‚ณใƒžใƒณใƒ‰ๅฑฅๆญด** - ้ŽๅŽปใซๅฎŸ่กŒใ—ใŸSSHใ‚ณใƒžใƒณใƒ‰ใฎ่‡ชๅ‹•่ฃœๅฎŒใจ่กจ็คบใŒๅฏ่ƒฝใงใ™ -- **ใ‚ฏใ‚คใƒƒใ‚ฏๆŽฅ็ถš** - ๆŽฅ็ถšใƒ‡ใƒผใ‚ฟใ‚’ไฟๅญ˜ใ›ใšใซใ‚ตใƒผใƒใƒผใซๆŽฅ็ถšใงใใพใ™ -- **ใ‚ณใƒžใƒณใƒ‰ใƒ‘ใƒฌใƒƒใƒˆ** - ๅทฆShiftใ‚ญใƒผใ‚’2ๅ›žๆŠผใ™ใ“ใจใงใ€ใ‚ญใƒผใƒœใƒผใƒ‰ใ‹ใ‚‰SSHๆŽฅ็ถšใซ็ด ๆ—ฉใใ‚ขใ‚ฏใ‚ปใ‚นใงใใพใ™ -- **Proxmox็ตฑๅˆ** - Proxmoxใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‹ใ‚‰ใƒ›ใ‚นใƒˆใ‚’่‡ชๅ‹•็š„ใซTermixใซ่ฟฝๅŠ ใงใใพใ™ -- **SSHๆฉŸ่ƒฝๅ……ๅฎŸ** - ใ‚ธใƒฃใƒณใƒ—ใƒ›ใ‚นใƒˆใ€Warpgateใ€TOTPใƒ™ใƒผใ‚นใฎๆŽฅ็ถšใ€SOCKS5ใ€ใƒ›ใ‚นใƒˆใ‚ญใƒผๆคœ่จผใ€ใƒ‘ใ‚นใƒฏใƒผใƒ‰่‡ชๅ‹•ๅ…ฅๅŠ›ใ€[OPKSSH](https://github.com/openpubkey/opkssh)ใ€tmuxใ€ใƒใƒผใƒˆๆ•ฒใ๏ผˆport knocking๏ผ‰ใ€ใ‚ฟใƒผใƒŸใƒŠใƒซใƒญใ‚ฐ่จ˜้Œฒใ€SSHใ‚จใƒผใ‚ธใ‚งใƒณใƒˆใƒ•ใ‚ฉใƒฏใƒผใƒ‡ใ‚ฃใƒณใ‚ฐใ€Bitwarden SSHใ‚จใƒผใ‚ธใ‚งใƒณใƒˆใ€HashiCorp Vault SSH็ฝฒๅใชใฉใซๅฏพๅฟœใ—ใฆใ„ใพใ™ -- **Termix ID** - Termixใซ็ต„ใฟ่พผใพใ‚ŒใŸsshid.io็›ธๅฝ“ใฎๆฉŸ่ƒฝใงใ™ใ€‚ใƒใƒณใƒ‰ใƒซใ‚’ๅ–ๅพ—ใ—ใ€ใƒชใ‚พใƒซใƒใƒผURLใงๅ…ฌ้–‹SSHใ‚ญใƒผใ‚’ๅ…ฌ้–‹ใ—ใ€็ต„ใฟ่พผใฟCAใ‚’ไฝฟ็”จใ—ใฆSSH่จผๆ˜Žๆ›ธใ‚’็™บ่กŒใงใใพใ™ใ€‚ +- **ใƒ€ใƒƒใ‚ทใƒฅใƒœใƒผใƒ‰** - ่‡ชๅˆ†ใงไธฆในใŸใ‚ซใƒผใƒ‰ใงใ‚ตใƒผใƒใƒผใฎ็Šถๆณใ‚’ใฒใจ็›ฎใงๆŠŠๆก +- **ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏๅ›ณ** - ใƒ›ใ‚นใƒˆใ‹ใ‚‰ใƒ›ใƒผใƒ ใƒฉใƒœใ‚’ๅ›ณใซใ—ใฆใ€็Šถๆ…‹ใ‚’ใƒชใ‚ขใƒซใ‚ฟใ‚คใƒ ่กจ็คบ +- **tmux ใƒขใƒ‹ใ‚ฟใƒผ** - tmux ใฎใ‚ปใƒƒใ‚ทใƒงใƒณใ€ใ‚ฆใ‚ฃใƒณใƒ‰ใ‚ฆใ€ใƒšใ‚คใƒณใ‚’ใƒ—ใƒฌใƒ“ใƒฅใƒผใจๆคœ็ดขไป˜ใใงไธ€่ฆง +- **API ใ‚ญใƒผ** - ใ‚นใ‚ฏใƒชใƒ—ใƒˆใ‚„ CI ็”จใฎใ€ๆœ‰ๅŠนๆœŸ้™ไป˜ใใƒฆใƒผใ‚ถใƒผๅ˜ไฝใฎใ‚ญใƒผ +- **ใ‚จใ‚ฏใ‚นใƒใƒผใƒˆใจใ‚คใƒณใƒใƒผใƒˆ** - ใƒ›ใ‚นใƒˆใ€่ช่จผๆƒ…ๅ ฑใ€ใƒ•ใ‚กใ‚คใƒซใƒžใƒใƒผใ‚ธใƒฃใƒผใฎใƒ‡ใƒผใ‚ฟใ‚’ๅ‡บใ—ๅ…ฅใ‚Œ +- **่‡ชๅ‹• SSL** - ่จผๆ˜Žๆ›ธใฎ็™บ่กŒใจๆ›ดๆ–ฐใ€HTTPS ใธใฎใƒชใƒ€ใ‚คใƒฌใ‚ฏใƒˆใ‚’่‡ชๅ‹•ใงใ€‚่‡ชๅ‰ใฎ่จผๆ˜Žๆ›ธใ‚‚ไฝฟใˆใพใ™ +- **ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚น** - ๆจ™ๆบ–ใฏ SQLiteใ€PostgreSQL ใจ MySQL ใซใ‚‚ๅฏพๅฟœ +- **ใƒขใƒ€ใƒณใช UI** - ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใงใ‚‚ใƒขใƒใ‚คใƒซใงใ‚‚ไฝฟใˆใ‚‹ React ใฎ็”ป้ขใ€‚ใƒฉใ‚คใƒˆใ€ใƒ€ใƒผใ‚ฏใ€Dracula ใชใฉใฎใƒ†ใƒผใƒžไป˜ใใ€‚ใฉใฎๆŽฅ็ถšใ‚‚ URL ใ‹ใ‚‰ใƒ•ใƒซใ‚นใ‚ฏใƒชใƒผใƒณใง้–‹ใ‘ใพใ™ +- **ใ‚ณใƒžใƒณใƒ‰ใƒ‘ใƒฌใƒƒใƒˆ** - ๅทฆ Shift ใฎ 2 ๅ›žๆŠผใ—ใงใ€ใ‚ญใƒผใƒœใƒผใƒ‰ใ‹ใ‚‰ใƒ›ใ‚นใƒˆใธ็งปๅ‹• +- **ใ‚ญใƒผใƒœใƒผใƒ‰ใ‚ทใƒงใƒผใƒˆใ‚ซใƒƒใƒˆ** - ใ‚ฟใƒ–ใฎ็งปๅ‹•ใ‚„้–‰ใ˜ใ‚‹ๆ“ไฝœใชใฉใ€ใ™ในใฆๅ‰ฒใ‚Šๅฝ“ใฆๅค‰ๆ›ดๅฏ่ƒฝ +- **Wake-on-LAN** - Termix ใ‹ใ‚‰ใงใ‚‚่‡ชๅ‹•ๅŒ–ใฎใ‚นใƒ†ใƒƒใƒ—ใ‹ใ‚‰ใงใ‚‚ใƒžใ‚ทใƒณใ‚’่ตทๅ‹• +- **ไฟก้ ผๆธˆใฟใƒ—ใƒญใ‚ญใ‚ท่ช่จผ** - ใƒชใƒใƒผใ‚นใƒ—ใƒญใ‚ญใ‚ทใซใ‚ตใ‚คใƒณใ‚คใƒณใ‚’ไปปใ›ใ€ใƒฆใƒผใ‚ถใƒผๆƒ…ๅ ฑใ‚’ๅผ•ใ็ถ™ใŽ +- **ๅ……ๅฎŸใ—ใŸ SSH ๆฉŸ่ƒฝ** - ่ธใฟๅฐใƒ›ใ‚นใƒˆใ€Warpgateใ€TOTP ใฎๅ…ฅๅŠ›ใ€SOCKS5ใ€ใƒ›ใ‚นใƒˆ้ตใฎๆคœ่จผใ€ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใฎ่‡ชๅ‹•ๅ…ฅๅŠ›ใ€[OPKSSH](https://github.com/openpubkey/opkssh)ใ€tmuxใ€ใƒใƒผใƒˆใƒŽใƒƒใ‚ญใƒณใ‚ฐใ€ใ‚ฟใƒผใƒŸใƒŠใƒซใฎใƒญใ‚ฐใ€ใ‚จใƒผใ‚ธใ‚งใƒณใƒˆ่ปข้€ใ€Bitwarden SSH ใ‚จใƒผใ‚ธใ‚งใƒณใƒˆใ€HashiCorp Vault ใฎ SSH ็ฝฒๅใชใฉ +- **Termix ID** - sshid.io ใฎใ‚ˆใ†ใชไป•็ต„ใฟใ‚’ๅ†…่”ตใ€‚ใƒใƒณใƒ‰ใƒซใ‚’ๅ–ๅพ—ใ—ใ€ๅ…ฌ้–‹้ตใ‚’ใƒชใ‚พใƒซใƒใƒผ URL ใงๅ…ฌ้–‹ใ—ใ€ๅ†…่”ต CA ใ‹ใ‚‰ SSH ่จผๆ˜Žๆ›ธใ‚’็™บ่กŒใงใใพใ™ @@ -224,15 +283,15 @@ Tailnetใฎใƒ‡ใƒใ‚คใ‚นใ‚’ใƒชใ‚นใƒˆใ—ใฆใƒ›ใ‚นใƒˆใจใ—ใฆใ™ใฐใ‚„ใ่ฟฝๅŠ ใ— Web -ใ‚ใ‚‰ใ‚†ใ‚‹ๆœ€ๆ–ฐใƒ–ใƒฉใ‚ฆใ‚ถ๏ผˆChromeใ€Safariใ€Firefox๏ผ‰ยท PWAๅฏพๅฟœ +ๆœ€่ฟ‘ใฎใƒ–ใƒฉใ‚ฆใ‚ถๅ…จ่ˆฌ๏ผˆChromeใ€Safariใ€Firefox๏ผ‰ยท PWA ๅฏพๅฟœ Windows x64/ia32 -ใƒใƒผใ‚ฟใƒ–ใƒซ็‰ˆ ยท MSIใ‚คใƒณใ‚นใƒˆใƒผใƒฉใƒผ ยท Chocolatey +ใƒใƒผใ‚ฟใƒ–ใƒซ ยท MSI ใ‚คใƒณใ‚นใƒˆใƒผใƒฉใƒผ ยท Chocolatey Linux x64/ia32 -ใƒใƒผใ‚ฟใƒ–ใƒซ็‰ˆ ยท AUR ยท AppImage ยท Deb ยท Flatpak +ใƒใƒผใ‚ฟใƒ–ใƒซ ยท AUR ยท AppImage ยท Deb ยท Flatpak macOS x64/ia32, v12.0+ @@ -252,9 +311,9 @@ Tailnetใฎใƒ‡ใƒใ‚คใ‚นใ‚’ใƒชใ‚นใƒˆใ—ใฆใƒ›ใ‚นใƒˆใจใ—ใฆใ™ใฐใ‚„ใ่ฟฝๅŠ ใ— ## ใ‚คใƒณใ‚นใƒˆใƒผใƒซ -ใ™ในใฆใฎใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ใธใฎTermixใฎใ‚คใƒณใ‚นใƒˆใƒผใƒซๆ–นๆณ•ใซใคใ„ใฆใฏใ€[Termixใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ](https://docs.termix.site/install)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ +ใ™ในใฆใฎใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ๅ‘ใ‘ใฎ่ฉณใ—ใ„ใ‚คใƒณใ‚นใƒˆใƒผใƒซๆ‰‹้ †ใฏ [Termix ใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ](https://docs.termix.site/install)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ -ใ‚ตใƒณใƒ—ใƒซDocker Composeใƒ•ใ‚กใ‚คใƒซ๏ผˆใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ๆฉŸ่ƒฝใ‚’ไฝฟ็”จใ™ใ‚‹ไบˆๅฎšใŒใชใ„ๅ ดๅˆใฏใ€`guacd`ใจใƒใƒƒใƒˆใƒฏใƒผใ‚ฏใฎ่จญๅฎšใ‚’็œ็•ฅใงใใพใ™๏ผ‰๏ผš +Docker Compose ใฎไพ‹ใงใ™๏ผˆใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ๆฉŸ่ƒฝใ‚’ไฝฟใ‚ใชใ„ใชใ‚‰ `guacd` ใจใƒใƒƒใƒˆใƒฏใƒผใ‚ฏใฎ้ƒจๅˆ†ใฏ็œ็•ฅใงใใพใ™๏ผ‰: ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### ใ‚ณใƒžใƒณใƒ‰ใƒฉใ‚คใƒณใƒ„ใƒผใƒซ + +Termix ใซใฏ CLI ใ‚‚ใ‚ใ‚‹ใฎใงใ€ใ‚ฟใƒผใƒŸใƒŠใƒซใ‹ใ‚‰ใ‚ตใƒผใƒใƒผใ‚’็ฎก็†ใ—ใŸใ‚Šใ€่‡ชๅˆ†ใฎใ‚นใ‚ฏใƒชใƒ—ใƒˆใซ็ต„ใฟ่พผใ‚“ใ ใ‚Šใงใใพใ™ใ€‚ + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +ใ‚ฟใƒผใƒŸใƒŠใƒซใ‚’้–‹ใใ€ใƒ›ใ‚นใƒˆ 1 ๅฐใพใŸใฏใƒ•ใƒชใƒผใƒˆๅ…จไฝ“ใงใ‚ณใƒžใƒณใƒ‰ใ‚’ๅฎŸ่กŒใ—ใ€SFTP ใงใƒ•ใ‚กใ‚คใƒซใ‚’็งปๅ‹•ใ—ใ€ใƒ›ใ‚นใƒˆใ‚„ใ‚นใƒ‹ใƒšใƒƒใƒˆใ€่ช่จผๆƒ…ๅ ฑใ‚’็ฎก็†ใงใใพใ™ใ€‚่ฉณใ—ใ„่ชฌๆ˜Žใฏ [docs.termix.site/cli](https://docs.termix.site/cli) ใซใ‚ใ‚Šใพใ™ใ€‚ + +### ใ‚ฏใƒฉใ‚ฆใƒ‰ใงใฎ้‹็”จ + +Termix ใฎใ‚ตใƒผใƒใƒผใฏ่‡ชๅˆ†ใฎใƒใƒƒใƒˆใƒฏใƒผใ‚ฏๅ†…ใงใฏใชใใ€VPS ใงๅ‹•ใ‹ใ™ใ“ใจใ‚‚ใงใใพใ™ใ€‚็ฎก็†ๅฏพ่ฑกใฎใƒใƒƒใƒˆใƒฏใƒผใ‚ฏไธŠใงๅ‹•ใ‹ใ—ใฆใ„ใ‚‹ใจใ€้šœๅฎณใŒ่ตทใใŸใจใใซ Termix ใ‚‚ไธ€็ท’ใซ่ฝใกใฆใ—ใพใ„ใ€็›ดใ—ใŸใ„ใจใใซ้™ใฃใฆไฝฟใˆใชใใชใ‚Šใพใ™ใ€‚ๅค–ใงๅ‹•ใ‹ใ—ใฆใŠใ‘ใฐใ„ใคใงใ‚‚ๅฑŠใใพใ™ใ—ใ€ๅ›บๅฎš IP ใ‚‚ๆ‰‹ใซๅ…ฅใ‚Šใ€VPN ใ‚„ใƒใƒผใƒˆ้–‹ๆ”พใชใ—ใงใฉใ“ใ‹ใ‚‰ใงใ‚‚ๅ…ฅใ‚Œใพใ™ใ€‚ + +[GINERNET](https://docs.termix.site/install/ginernet) ใฏ Termix ใฎใ‚นใƒใƒณใ‚ตใƒผใงใ€ๅŒ็คพใฎ VPS ใธใƒ‡ใƒ—ใƒญใ‚คใ™ใ‚‹ๆ‰‹้ †ใฏใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆใซ่ฉณใ—ใ่ผ‰ใฃใฆใ„ใพใ™ใ€‚ + +
+ +## ใƒ†ใƒฌใƒกใƒˆใƒชใƒผ + +Termix ใฏ 1 ๆ—ฅ 1 ๅ›žใ€ๅŒฟๅใฎๅฐใ•ใชใƒ‡ใƒผใ‚ฟใ‚’้€ใ‚Šใพใ™ใ€‚ใฉใ‚Œใใ‚‰ใ„ใฎใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใŒๅ‹•ใ„ใฆใ„ใฆใ€ใฉใฎๆฉŸ่ƒฝใŒไฝฟใ‚ใ‚Œใฆใ„ใ‚‹ใ‹ใ‚’ๆŠŠๆกใ™ใ‚‹ใŸใ‚ใฎใ‚‚ใฎใงใ™ใ€‚ๅซใพใ‚Œใ‚‹ใฎใฏใƒฉใƒณใƒ€ใƒ ใชใ‚คใƒณใ‚นใ‚ฟใƒณใ‚น IDใ€ใƒฆใƒผใ‚ถใƒผใจใƒ›ใ‚นใƒˆใฎๆ•ฐใ€ใ‚ขใƒ—ใƒชใฎใƒใƒผใ‚ธใƒงใƒณใ€็›ด่ฟ‘ 24 ๆ™‚้–“ใซไฝฟใ‚ใ‚ŒใŸๆฉŸ่ƒฝ๏ผˆใ‚ฟใƒผใƒŸใƒŠใƒซใ€ใƒ•ใ‚กใ‚คใƒซใƒžใƒใƒผใ‚ธใƒฃใƒผใ€ใƒˆใƒณใƒใƒซใ€Docker ใชใฉ๏ผ‰ใ ใ‘ใงใ™ใ€‚ใƒฆใƒผใ‚ถใƒผๅใ€ใƒ›ใ‚นใƒˆๅใ€IP ใ‚ขใƒ‰ใƒฌใ‚นใ€่ช่จผๆƒ…ๅ ฑใชใฉใ€ใ‚ใชใŸใ‚„ใ‚ตใƒผใƒใƒผใ‚’็‰นๅฎšใงใใ‚‹ใ‚‚ใฎใฏไธ€ๅˆ‡ๅซใพใ‚Œใพใ›ใ‚“ใ€‚ + +ๅˆๆœŸ็Šถๆ…‹ใงใฏๆœ‰ๅŠนใงใ™ใ€‚็ฎก็†่จญๅฎšใฎใ€Œไธ€่ˆฌใ€ใ‹ใ‚‰ๆญขใ‚ใ‚‰ใ‚Œใพใ™ใ—ใ€Termix ใ‚’่ตทๅ‹•ใ™ใ‚‹ๅ‰ใซ `ENABLE_TELEMETRY=false` ใ‚’่จญๅฎšใ—ใฆใŠใใ“ใจใ‚‚ใงใใพใ™ใ€‚ +
## ๅฏ„ไป˜ -Termixใฏ็„กๆ–™ใฎใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใงใ‚ใ‚Šใ€ใ‚ตใƒ–ใ‚นใ‚ฏใƒชใƒ—ใ‚ทใƒงใƒณใ‚„ๆœ‰ๆ–™ใƒ—ใƒฉใƒณใฏใ‚ใ‚Šใพใ›ใ‚“ใ€‚ไพฟๅˆฉใ ใจๆ„Ÿใ˜ใŸๅ ดๅˆใฏใ€ใ‚ตใƒผใƒใƒผใ‚ณใ‚นใƒˆใ€ใƒ‰ใƒกใ‚คใƒณใ€้–‹็™บๆ™‚้–“ใ‚’่ณ„ใ†ใŸใ‚ใฎๅฏ„ไป˜ใ‚’ใ”ๆคœ่จŽใใ ใ•ใ„ใ€‚ๅฏ„ไป˜ใฏใ€SAMLใ€Kubernetesใ€Agentใ‚ตใƒใƒผใƒˆใชใฉใฎๆฉŸ่ƒฝใ‚’ๆง‹็ฏ‰ใ™ใ‚‹ใŸใ‚ใซๅฟ…่ฆใช่ชฟๆŸปใจๅญฆ็ฟ’ใฎๆ™‚้–“ใ‚’็ขบไฟใ™ใ‚‹ใ“ใจใซใ‚‚ๅฝน็ซ‹ใกใพใ™ใ€‚ไปฅไธ‹ใง้€ฒๆ—ใ‚’็ขบ่ชใ—ใ€ๅฏ„ไป˜ใงใใพใ™ใ€‚ +Termix ใฏ็„กๆ–™ใงใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใงใ€ใ‚ตใƒ–ใ‚นใ‚ฏใƒชใƒ—ใ‚ทใƒงใƒณใ‚‚ๆœ‰ๆ–™ใƒ—ใƒฉใƒณใ‚‚ใ‚ใ‚Šใพใ›ใ‚“ใ€‚ๅฝนใซ็ซ‹ใฃใฆใ„ใ‚‹ใจๆ„Ÿใ˜ใŸใ‚‰ใ€ใ‚ตใƒผใƒใƒผไปฃใ€ใƒ‰ใƒกใ‚คใƒณใ€้–‹็™บๆ™‚้–“ใ‚’ๆ”ฏใˆใ‚‹ใŸใ‚ใฎๅฏ„ไป˜ใ‚’ใ”ๆคœ่จŽใใ ใ•ใ„ใ€‚ๅฏ„ไป˜ใฏ SAMLใ€Kubernetesใ€ใ‚จใƒผใ‚ธใ‚งใƒณใƒˆๅฏพๅฟœใจใ„ใฃใŸๆฉŸ่ƒฝใ‚’ไฝœใ‚‹ใŸใ‚ใฎ่ชฟๆŸปใ‚„ๅญฆ็ฟ’ใฎๆ™‚้–“ใซใ‚‚ใ‚ใฆใ‚‰ใ‚Œใพใ™ใ€‚้€ฒใฟๅ…ทๅˆใฎ็ขบ่ชใจๅฏ„ไป˜ใฏไธ‹่จ˜ใ‹ใ‚‰ใฉใ†ใžใ€‚ [ๅฏ„ไป˜ใ™ใ‚‹](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termixใฏ็„กๆ–™ใฎใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใงใ‚ใ‚Šใ€ใ‚ตใƒ–ใ‚น ## ใ‚นใƒใƒณใ‚ตใƒผ -้–‹็™บใ‚’ๆ”ฏๆดใ™ใ‚‹ใŸใ‚ใฎๆœ‰ๆ–™ๆŽฒ่ผ‰ใซใ”่ˆˆๅ‘ณใŒใ‚ใ‚Šใพใ™ใ‹๏ผŸ[mail@termix.site](mailto:mail@termix.site)ใพใงใƒกใƒผใƒซใ‚’ใŠ้€ใ‚Šใใ ใ•ใ„ใ€‚ +ๆœ‰ๆ–™ๆŽฒ่ผ‰ใง้–‹็™บใ‚’ๆ”ฏๆดใ™ใ‚‹ใ“ใจใซใ”่ˆˆๅ‘ณใŒใ‚ใ‚Šใพใ™ใ‹ใ€‚[mail@termix.site](mailto:mail@termix.site) ใพใงใ”้€ฃ็ตกใใ ใ•ใ„ใ€‚
@@ -325,10 +410,6 @@ Termixใฏ็„กๆ–™ใฎใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใงใ‚ใ‚Šใ€ใ‚ตใƒ–ใ‚น Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Termixใฏ็„กๆ–™ใฎใ‚ชใƒผใƒ—ใƒณใ‚ฝใƒผใ‚นใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใงใ‚ใ‚Šใ€ใ‚ตใƒ–ใ‚น Rack Genius - +    + + Ginernet +

## ใ‚ตใƒใƒผใƒˆ -Termixใซ้–ขใ™ใ‚‹ใƒ˜ใƒซใƒ—ใ‚„ๆฉŸ่ƒฝใƒชใ‚ฏใ‚จใ‚นใƒˆใŒๅฟ…่ฆใชๅ ดๅˆใฏใ€[Issues](https://github.com/Termix-SSH/Support/issues)ใƒšใƒผใ‚ธใซใ‚ขใ‚ฏใ‚ปใ‚นใ—ใ€ใƒญใ‚ฐใ‚คใƒณใ—ใฆ`New Issue`ใ‚’ๆŠผใ—ใฆใใ ใ•ใ„ใ€‚Issueใฏใงใใ‚‹ใ ใ‘่ฉณ็ดฐใซ่จ˜่ฟฐใ—ใ€่‹ฑ่ชžใงใฎ่จ˜่ฟฐใŒๆœ›ใพใ—ใ„ใงใ™ใ€‚ใพใŸใ€[Discord](https://discord.gg/jVQGdvHDrf)ใ‚ตใƒผใƒใƒผใซๅ‚ๅŠ ใ—ใฆใ‚ตใƒใƒผใƒˆใƒใƒฃใƒณใƒใƒซใ‚’ๅˆฉ็”จใ™ใ‚‹ใ“ใจใ‚‚ใงใใพใ™ใŒใ€ๅฟœ็ญ”ๆ™‚้–“ใŒ้•ทใใชใ‚‹ๅ ดๅˆใŒใ‚ใ‚Šใพใ™ใ€‚ +ๅ›ฐใฃใŸใจใใ‚„ๆฉŸ่ƒฝใฎ่ฆๆœ›ใŒใ‚ใ‚‹ใจใใฏใ€[ๆ–ฐใ—ใ„ issue](https://github.com/Termix-SSH/Support/issues) ใ‚’ไฝœใฃใฆใ€ใงใใ‚‹ใ ใ‘่ฉณใ—ใใ€ใงใใ‚Œใฐ่‹ฑ่ชžใงๆ›ธใ„ใฆใใ ใ•ใ„ใ€‚[Discord](https://discord.gg/jVQGdvHDrf) ใฎใ‚ตใƒใƒผใƒˆใƒใƒฃใƒณใƒใƒซใงใ‚‚่ณชๅ•ใงใใพใ™ใŒใ€่ฟ”ไฟกใซใฏๆ™‚้–“ใŒใ‹ใ‹ใ‚‹ใ“ใจใŒใ‚ใ‚Šใพใ™ใ€‚
@@ -359,7 +443,7 @@ Termixใซ้–ขใ™ใ‚‹ใƒ˜ใƒซใƒ—ใ‚„ๆฉŸ่ƒฝใƒชใ‚ฏใ‚จใ‚นใƒˆใŒๅฟ…่ฆใชๅ ดๅˆใฏใ€[Issu [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -YouTubeใงใ‚ขใƒƒใƒ—ใƒ‡ใƒผใƒˆใฎๆฆ‚่ฆใ‚’่ฆ–่ดใ™ใ‚‹ +YouTube ใงใ‚ขใƒƒใƒ—ใƒ‡ใƒผใƒˆใฎ็ดนไป‹ใ‚’่ฆ‹ใ‚‹

@@ -399,7 +483,7 @@ Termixใซ้–ขใ™ใ‚‹ใƒ˜ใƒซใƒ—ใ‚„ๆฉŸ่ƒฝใƒชใ‚ฏใ‚จใ‚นใƒˆใŒๅฟ…่ฆใชๅ ดๅˆใฏใ€[Issu -ๅ‹•็”ปใ‚„็”ปๅƒใฎไธ€้ƒจใฏๆœ€ๆ–ฐใงใฏใชใ„ๅ ดๅˆใ‚„ใ€ๆฉŸ่ƒฝใ‚’ๅฎŒๅ…จใซ็ดนไป‹ใงใใฆใ„ใชใ„ๅ ดๅˆใŒใ‚ใ‚Šใพใ™ใ€‚ +ๅ‹•็”ปใ‚„็”ปๅƒใฏๅคใใชใฃใฆใ„ใŸใ‚Šใ€ๆฉŸ่ƒฝใ‚’ๅๅˆ†ใซไผใˆใ‚‰ใ‚Œใฆใ„ใชใ„ๅ ดๅˆใŒใ‚ใ‚Šใพใ™ใ€‚ @@ -407,10 +491,10 @@ Termixใซ้–ขใ™ใ‚‹ใƒ˜ใƒซใƒ—ใ‚„ๆฉŸ่ƒฝใƒชใ‚ฏใ‚จใ‚นใƒˆใŒๅฟ…่ฆใชๅ ดๅˆใฏใ€[Issu ## ไบˆๅฎšใ•ใ‚Œใฆใ„ใ‚‹ๆฉŸ่ƒฝ -ใ™ในใฆใฎไบˆๅฎšๆฉŸ่ƒฝใซใคใ„ใฆใฏ[Projects](https://github.com/orgs/Termix-SSH/projects/5)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ใ‚ณใƒณใƒˆใƒชใƒ“ใƒฅใƒผใƒˆใ‚’ใ”ๅธŒๆœ›ใฎๆ–นใฏ[Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ +ไบˆๅฎšใ•ใ‚Œใฆใ„ใ‚‹ๆฉŸ่ƒฝใฏใ™ในใฆ [Projects](https://github.com/orgs/Termix-SSH/projects/5) ใซใ‚ใ‚Šใพใ™ใ€‚่ฒข็Œฎใ‚’ใŠ่€ƒใˆใฎๆ–นใฏ[ใ‚ณใƒณใƒˆใƒชใƒ“ใƒฅใƒผใ‚ทใƒงใƒณใ‚ฌใ‚คใƒ‰](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚
## ใƒฉใ‚คใ‚ปใƒณใ‚น -Apache License Version 2.0ใฎใ‚‚ใจใง้…ๅธƒใ•ใ‚Œใฆใ„ใพใ™ใ€‚่ฉณ็ดฐใฏ`LICENSE`ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ +Apache License 2.0 ใฎใ‚‚ใจใง้…ๅธƒใ—ใฆใ„ใพใ™ใ€‚่ฉณใ—ใใฏ `LICENSE` ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚ diff --git a/docs/readme/README-KO.md b/docs/readme/README-KO.md index 8382f83..ba157af 100644 --- a/docs/readme/README-KO.md +++ b/docs/readme/README-KO.md @@ -4,7 +4,7 @@

Termix

-

์…€ํ”„ ํ˜ธ์ŠคํŒ… SSH ๊ด€๋ฆฌ ๋ฐ ์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ ์•ก์„ธ์Šค

+

SSH์™€ ์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ๋ถ€ํ„ฐ ์ž๋™ํ™”๊นŒ์ง€, ์…€ํ”„ ํ˜ธ์ŠคํŒ… ์„œ๋ฒ„ ๊ด€๋ฆฌ

English ยท @@ -37,7 +37,7 @@
-Termix๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ์„œ๋ฒ„ ๋น„์šฉ๊ณผ ๊ฐœ๋ฐœ ์‹œ๊ฐ„์„ ์œ„ํ•ด [ํ›„์›](https://donate.termix.site/)์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”. +Termix๋Š” ๋ฌด๋ฃŒ์ด๋ฉฐ ์˜คํ”ˆ ์†Œ์Šค์ž…๋‹ˆ๋‹ค. ์œ ์šฉํ•˜๊ฒŒ ์“ฐ๊ณ  ๊ณ„์‹œ๋‹ค๋ฉด ์„œ๋ฒ„ ๋น„์šฉ๊ณผ ๊ฐœ๋ฐœ ์‹œ๊ฐ„์— ๋ณดํƒฌ์ด ๋˜๋„๋ก [ํ›„์›](https://donate.termix.site/)์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.
@@ -58,7 +58,7 @@ Termix๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ## ๊ฐœ์š” -Termix๋Š” ์˜คํ”ˆ ์†Œ์Šค์ด๋ฉฐ ์˜๊ตฌ ๋ฌด๋ฃŒ์ธ ์…€ํ”„ ํ˜ธ์ŠคํŒ… ์˜ฌ์ธ์› ์„œ๋ฒ„ ๊ด€๋ฆฌ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. ๋‹จ์ผ ์ง๊ด€์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์™€ ์ธํ”„๋ผ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ€ํ‹ฐ ํ”Œ๋žซํผ ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Termix๋Š” SSH ํ„ฐ๋ฏธ๋„ ์ ‘์†, ์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ ์ œ์–ด(RDP, VNC, Telnet), SSH ํ„ฐ๋„๋ง ๊ธฐ๋Šฅ, ์›๊ฒฉ SSH ํŒŒ์ผ ๊ด€๋ฆฌ ๋ฐ ๊ธฐํƒ€ ๋‹ค์–‘ํ•œ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Termix๋Š” ๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Termius์˜ ์™„๋ฒฝํ•œ ๋ฌด๋ฃŒ ์…€ํ”„ ํ˜ธ์ŠคํŒ… ๋Œ€์•ˆ์ž…๋‹ˆ๋‹ค. +Termix๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ ์†Œ์Šค ์…€ํ”„ ํ˜ธ์ŠคํŒ… ์„œ๋ฒ„ ๊ด€๋ฆฌ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. SSH ํ„ฐ๋ฏธ๋„, ์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ(RDP, VNC, Telnet), ํŒŒ์ผ ์ „์†ก, ํ„ฐ๋„, Docker, ์ง€ํ‘œ, ์ž๋™ํ™”๋ฅผ ํ•œ๊ณณ์— ๋ชจ์•„ ์›น๊ณผ ๋ฐ์Šคํฌํ†ฑ, ๋ชจ๋ฐ”์ผ์—์„œ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ„์† ๋ฌด๋ฃŒ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์…€ํ”„ ํ˜ธ์ŠคํŒ… Termius ๋Œ€์•ˆ์ž…๋‹ˆ๋‹ค.
@@ -68,126 +68,182 @@ Termix๋Š” ์˜คํ”ˆ ์†Œ์Šค์ด๋ฉฐ ์˜๊ตฌ ๋ฌด๋ฃŒ์ธ ์…€ํ”„ ํ˜ธ์ŠคํŒ… ์˜ฌ์ธ์› ์„œ๋ฒ„ -**SSH ํ„ฐ๋ฏธ๋„ ์ ‘์†:** -๋ธŒ๋ผ์šฐ์ € ์Šคํƒ€์ผ ํƒญ ์‹œ์Šคํ…œ๊ณผ ๋ถ„ํ•  ํ™”๋ฉด ์ง€์›(์ตœ๋Œ€ 4๊ฐœ ํŒจ๋„)์„ ๊ฐ–์ถ˜ ์™„์ „ํ•œ ๊ธฐ๋Šฅ์˜ ํ„ฐ๋ฏธ๋„. ์ผ๋ฐ˜ ํ„ฐ๋ฏธ๋„ ํ…Œ๋งˆ, ๊ธ€๊ผด ๋ฐ ๊ธฐํƒ€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํฌํ•จํ•œ ํ„ฐ๋ฏธ๋„ ์‚ฌ์šฉ์ž ์ •์˜ ์ง€์›. +**SSH ํ„ฐ๋ฏธ๋„:** +๋ธŒ๋ผ์šฐ์ € ๊ฐ™์€ ํƒญ๊ณผ ๋ถ„ํ•  ํ™”๋ฉด์„ ๊ฐ–์ถ˜ ์ œ๋Œ€๋กœ ๋œ ํ„ฐ๋ฏธ๋„๋กœ, ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 6๊ฐœ ํŒจ๋„๊นŒ์ง€ ๋„์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ๋งˆ์™€ ๊ธ€๊ผด, ์ƒ‰์„ ๊ณจ๋ผ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์„ธ์…˜ ์œ„์˜ ํˆด๋ฐ”์—๋Š” CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‘œ์‹œ๋˜๊ณ , ํ•ด๋‹น ํ˜ธ์ŠคํŠธ์˜ ํŒŒ์ผ๊ณผ Docker, ํ„ฐ๋„, ์ง€ํ‘œ๋กœ ๋ฐ”๋กœ ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ ์ ‘์†:** -์™„์ „ํ•œ ์‚ฌ์šฉ์ž ์ •์˜์™€ ๋ถ„ํ•  ํ™”๋ฉด์„ ์ง€์›ํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ฐ˜ RDP, VNC, Telnet ์ง€์›. +**์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ:** +๋ธŒ๋ผ์šฐ์ €์—์„œ RDP์™€ VNC, Telnet์„ ์“ธ ์ˆ˜ ์žˆ๊ณ  ๋‹ค๋ฅธ ์„ธ์…˜๊ณผ ๋˜‘๊ฐ™์ด ํƒญ๊ณผ ๋ถ„ํ•  ํ™”๋ฉด์œผ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. RDP ๋“œ๋ผ์ด๋ธŒ์šฉ ํŒŒ์ผ ๋ธŒ๋ผ์šฐ์ €์™€ ๋Œ์–ด๋‹ค ๋†“๊ธฐ ์—…๋กœ๋“œ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. Windows ๋ฐ์Šคํฌํ†ฑ์—์„œ๋Š” ํ˜ธ์ŠคํŠธ๋ฅผ ๊ธฐ๋ณธ RDP ํด๋ผ์ด์–ธํŠธ๋กœ ์—ด ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. -**SSH ํ„ฐ๋„ ๊ด€๋ฆฌ:** -์ž๋™ ์žฌ์—ฐ๊ฒฐ, ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง, ๋กœ์ปฌยท์›๊ฒฉยท๋™์  SOCKS ํฌ์›Œ๋”ฉ์„ ์ง€์›ํ•˜๋Š” ์„œ๋ฒ„ ๊ฐ„ SSH ํ„ฐ๋„ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ. ๋ฐ์Šคํฌํ†ฑ ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ํ„ฐ๋„ ์„ค์ •์€ ๋ฐ์Šคํฌํ†ฑ ์„ค์น˜๋ณ„๋กœ ๋กœ์ปฌ์— ์ €์žฅ๋˜๋ฉฐ, ์„ ํƒ์  C2S ์‚ฌ์ „ ์„ค์ • ์Šค๋ƒ…์ƒท์„ ์„œ๋ฒ„์— ์ €์žฅยท์ด๋ฆ„ ๋ณ€๊ฒฝยท๋ถˆ๋Ÿฌ์˜ค๊ธฐยท์‚ญ์ œํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ ๊ฐ„ ๋กœ์ปฌ ํ„ฐ๋„ ๊ตฌ์„ฑ์„ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +**SSH ํ„ฐ๋„:** +๋กœ์ปฌ๊ณผ ์›๊ฒฉ, ๋™์  SOCKS ํฌ์›Œ๋”ฉ์„ ์ง€์›ํ•˜๋ฉฐ ์ž๋™ ์žฌ์—ฐ๊ฒฐ๊ณผ ์ƒํƒœ ํ™•์ธ์ด ๋ถ™์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์Šคํฌํ†ฑ ์•ฑ์˜ ํด๋ผ์ด์–ธํŠธ ๋Œ€ ์„œ๋ฒ„ ํ„ฐ๋„์€ ๊ทธ ์ปดํ“จํ„ฐ์— ์ €์žฅ๋˜๊ณ , ํ”„๋ฆฌ์…‹์„ ์„œ๋ฒ„์— ์ €์žฅํ•ด ๋‘๋ฉด ๋‹ค๋ฅธ ์ปดํ“จํ„ฐ๋กœ ์„ค์ •์„ ์˜ฎ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**์›๊ฒฉ ํŒŒ์ผ ๊ด€๋ฆฌ์ž:** -์ฝ”๋“œ, ์ด๋ฏธ์ง€, ์˜ค๋””์˜ค, ๋น„๋””์˜ค์˜ ๋ณด๊ธฐ ๋ฐ ํŽธ์ง‘์„ ์ง€์›ํ•˜์—ฌ ์›๊ฒฉ ์„œ๋ฒ„์—์„œ ํŒŒ์ผ์„ ์ง์ ‘ ๊ด€๋ฆฌ. sudo ์ง€์›์œผ๋กœ ํŒŒ์ผ ์—…๋กœ๋“œ, ๋‹ค์šด๋กœ๋“œ, ์ด๋ฆ„ ๋ณ€๊ฒฝ, ์‚ญ์ œ, ์ด๋™์„ ์›ํ™œํ•˜๊ฒŒ ์ˆ˜ํ–‰. ์„œ๋ฒ„ ๊ฐ„ ํŒŒ์ผ ์ด๋™๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. +**ํŒŒ์ผ ๊ด€๋ฆฌ์ž:** +SFTP๋กœ ํŒŒ์ผ์„ ์‚ดํŽด๋ณด๊ณ  ํŽธ์ง‘ํ•˜๊ณ  ์˜ฌ๋ฆฌ๊ณ  ๋‚ด๋ ค๋ฐ›๊ณ  ์ด๋ฆ„์„ ๋ฐ”๊พธ๊ณ  ์˜ฎ๊ธฐ๊ณ  ์ง€์šธ ์ˆ˜ ์žˆ์œผ๋ฉฐ sudo๋„ ๋ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ์™€ ์ด๋ฏธ์ง€, ์˜ค๋””์˜ค, ๋น„๋””์˜ค๋ฅผ ๋ณด๊ณ  ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ์„œ๋ฒ„๋กœ ํŒŒ์ผ์„ ๋ฐ”๋กœ ๋ณต์‚ฌํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ๊ฐ€์žฅ ๋น ๋ฅธ ๊ฒฝ๋กœ๊ฐ€ ์ž๋™์œผ๋กœ ์„ ํƒ๋˜๊ณ  ์ „์†ก ๋ฌด๊ฒฐ์„ฑ๋„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. -**Docker ๋ฐ Podman ๊ด€๋ฆฌ:** -์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘, ์ค‘์ง€, ์ผ์‹œ ์ •์ง€, ์ œ๊ฑฐ. ์ปจํ…Œ์ด๋„ˆ ํ†ต๊ณ„ ๋ณด๊ธฐ. docker exec ํ„ฐ๋ฏธ๋„๋กœ ์ปจํ…Œ์ด๋„ˆ ์ œ์–ด. Docker์™€ Podman์„ ๋ชจ๋‘ ์ปจํ…Œ์ด๋„ˆ ๋Ÿฐํƒ€์ž„์œผ๋กœ ์ง€์›. Portainer๋‚˜ Dockge๋ฅผ ๋Œ€์ฒดํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ๋ณด๋‹ค๋Š” ๊ฐ„ํŽธํ•œ ๊ด€๋ฆฌ๋ฅผ ๋ชฉ์ ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. +**Docker์™€ Podman:** +์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‹œ์ž‘ํ•˜๊ณ  ๋ฉˆ์ถ”๊ณ  ์ผ์‹œ ์ •์ง€ํ•˜๊ณ  ์ง€์šธ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ƒํƒœ๋ฅผ ๋ณด๊ฑฐ๋‚˜ ์•ˆ์—์„œ ์…ธ์„ ์—ด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Docker์™€ Podman ๋ชจ๋‘์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. Portainer๋‚˜ Dockge๋ฅผ ๋Œ€์‹ ํ•˜๋ ค๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ด๋ฏธ ์žˆ๋Š” ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. -**SSH ํ˜ธ์ŠคํŠธ ๊ด€๋ฆฌ์ž:** -ํƒœ๊ทธ์™€ ํด๋”(ํด๋” ์‚ฌ์šฉ์ž ์ง€์ • ๋ฐ ์ค‘์ฒฉ ํด๋” ์ง€์›)๋กœ SSH ์—ฐ๊ฒฐ์„ ์ €์žฅ, ์ •๋ฆฌ, ๊ด€๋ฆฌํ•˜๊ณ , ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ์ €์žฅํ•˜๋ฉด์„œ SSH ํ‚ค ๋ฐฐํฌ๋ฅผ ์ž๋™ํ™”. +**ํ˜ธ์ŠคํŠธ ๊ด€๋ฆฌ:** +ํƒœ๊ทธ์™€, ์ด๋ฆ„๊ณผ ์ƒ‰์„ ๋ถ™์ผ ์ˆ˜ ์žˆ๋Š” ์ค‘์ฒฉ ํด๋”๋กœ ํ˜ธ์ŠคํŠธ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅํ•œ ์ž๊ฒฉ ์ฆ๋ช…์„ ์—ฌ๋Ÿฌ ํ˜ธ์ŠคํŠธ์—์„œ ๋‹ค์‹œ ์“ฐ๊ณ , SSH ํ‚ค๋ฅผ ์ž๋™์œผ๋กœ ๋ฐฐํฌํ•˜๊ณ , ํ˜ธ์ŠคํŠธ๋ฅผ ์ƒ์œ„ ํ˜ธ์ŠคํŠธ ์•„๋ž˜๋กœ ๋ฌถ๊ณ , ํ•œ๊บผ๋ฒˆ์— ํŽธ์ง‘ํ•˜๊ฑฐ๋‚˜ ๋‚ด๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅํ•˜๊ณ  ์‹ถ์ง€ ์•Š์€ ์ผํšŒ์„ฑ ์—ฐ๊ฒฐ์—๋Š” ๋น ๋ฅธ ์—ฐ๊ฒฐ์„ ์“ฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค. -**ํ˜ธ์ŠคํŠธ ๋ฉ”ํŠธ๋ฆญ:** -๋Œ€๋ถ€๋ถ„์˜ Linux ๊ธฐ๋ฐ˜ ์„œ๋ฒ„์—์„œ CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ ์‚ฌ์šฉ๋Ÿ‰, ๋„คํŠธ์›Œํฌ, ์—…ํƒ€์ž„, ์‹œ์Šคํ…œ ์ •๋ณด, ๋ฐฉํ™”๋ฒฝ, ํฌํŠธ ๋ชจ๋‹ˆํ„ฐ, ๋กœ๊ทธ ๋ทฐ์–ด, ์‚ฌ์šฉ์ž/๊ถŒํ•œ, ์ธ์ฆ์„œ ๋“ฑ ๋‹ค์–‘ํ•œ ์ •๋ณด๋ฅผ ํ‘œ์‹œ. ์‹œ๊ณ„์—ด ํžˆ์Šคํ† ๋ฆฌ ๊ทธ๋ž˜ํ”„์™€ ntfy ๋ฐ ์›นํ›…์„ ์ง€์›ํ•˜๋Š” ์ž„๊ณ„๊ฐ’ ๊ธฐ๋ฐ˜ ์•Œ๋ฆผ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. +**ํ˜ธ์ŠคํŠธ ์ง€ํ‘œ:** +๋Œ€๋ถ€๋ถ„์˜ ๋ฆฌ๋ˆ…์Šค ์„œ๋ฒ„์—์„œ CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ, ๋„คํŠธ์›Œํฌ, ์˜จ๋„, ๊ฐ€๋™ ์‹œ๊ฐ„, ํ”„๋กœ์„ธ์Šค, ํฌํŠธ, ๋กœ๊ทธ์ธ, ์‹œ์Šคํ…œ ์ •๋ณด๋ฅผ ๊ธฐ๋ก ๊ทธ๋ž˜ํ”„์™€ ํ•จ๊ป˜ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ ์นด๋“œ๋กœ ์„œ๋น„์Šค์™€ cron ์ž‘์—…, ํŒจํ‚ค์ง€, ์‚ฌ์šฉ์ž, ๋ฐฉํ™”๋ฒฝ ๊ทœ์น™, WireGuard, Tailscale, SSL ์ธ์ฆ์„œ, ๋กœ๊ทธ, ์ƒํƒœ ํ™•์ธ์„ Termix ์•ˆ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**์‚ฌ์šฉ์ž ์ธ์ฆ:** -๊ด€๋ฆฌ์ž ์ œ์–ด(๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ์ •๋ณด ํŽธ์ง‘ ๊ฐ€๋Šฅ)์™€ OIDC/LDAP/SSO(์•ก์„ธ์Šค ์ œ์–ด ํฌํ•จ), 2FA(TOTP), ํŒจ์Šคํ‚ค(WebAuthn) ์ง€์›์„ ํ†ตํ•œ ์•ˆ์ „ํ•œ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ. ๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ํ™œ์„ฑ ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ๋ณด๊ณ  ๊ถŒํ•œ์„ ์ทจ์†Œ ๊ฐ€๋Šฅ. OIDC/๋กœ์ปฌ ๊ณ„์ • ์—ฐ๋™. ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž‘์—…์˜ ๊ฐ์‚ฌ ๋กœ๊ทธ ์กฐํšŒ. +**์ž๋™ํ™”:** +๋จผ์ € ์กฐ๊ฑด์„ ๊ณ ๋ฅด๊ณ , ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚ ์ง€ ์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด์—๋Š” ์ง€ํ‘œ๊ฐ€ ๊ธฐ์ค€์„ ๋„˜์„ ๋•Œ, ํ˜ธ์ŠคํŠธ๊ฐ€ ์˜ฌ๋ผ์˜ค๊ฑฐ๋‚˜ ๋‚ด๋ ค๊ฐˆ ๋•Œ, ์ƒํƒœ ํ™•์ธ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ”๋€” ๋•Œ, ์ •ํ•ด์ง„ ์ผ์ •, ์ปจํ…Œ์ด๋„ˆ ์ด๋ฒคํŠธ, ๋“ค์–ด์˜ค๋Š” ์›นํ›…์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„์—์„œ ๋ช…๋ น๊ณผ ์Šค๋‹ˆํŽซ์„ ์‹คํ–‰ํ•˜๊ณ , ์ปจํ…Œ์ด๋„ˆ์™€ ํ„ฐ๋„์„ ์กฐ์ž‘ํ•˜๊ณ , ํ˜ธ์ŠคํŠธ๋ฅผ ๊นจ์šฐ๊ณ , URL์„ ํ˜ธ์ถœํ•˜๊ณ , ๊ธฐ๋‹ค๋ฆฌ๊ณ , ์กฐ๊ฑด์— ๋”ฐ๋ผ ๊ฐˆ๋ผ์ง€๊ณ , ๋‹ค๋ฅธ ์ž๋™ํ™”๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ntfy๋‚˜ Discord, ์›นํ›…์œผ๋กœ ์•Œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์‹คํ–‰์œผ๋กœ ๋จผ์ € ์•ˆ์ „ํ•˜๊ฒŒ ์‹œํ—˜ํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**Tailscale ํ†ตํ•ฉ:** -Tailscale ๋„คํŠธ์›Œํฌ์˜ ๊ธฐ๊ธฐ๋ฅผ ๋‚˜์—ดํ•˜์—ฌ ํ˜ธ์ŠคํŠธ๋กœ ๋น ๋ฅด๊ฒŒ ์ถ”๊ฐ€ํ•˜๊ณ , Tailscale SSH๋ฅผ ์ธ์ฆ ๋ฐฉ๋ฒ•์œผ๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์—ฐ๊ฒฐํ•จ์œผ๋กœ์จ ์ž๊ฒฉ ์ฆ๋ช…์„ ์ €์žฅํ•˜์ง€ ์•Š๊ณ ๋„ ๋„คํŠธ์›Œํฌ ACL์ด ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. +**ํ”Œ๋ฆฟ:** +ํ˜ธ์ŠคํŠธ๋ฅผ ์ง์ ‘ ๊ณ ๋ฅด๊ฑฐ๋‚˜ ํƒœ๊ทธ ๊ทœ์น™์œผ๋กœ ํ”Œ๋ฆฟ์— ๋ฌถ์œผ๋ฉด ์ƒˆ ํ˜ธ์ŠคํŠธ๋Š” ์•Œ์•„์„œ ๋“ค์–ด์˜ต๋‹ˆ๋‹ค. ๋ชจ๋“  ํ˜ธ์ŠคํŠธ์—์„œ ๊ฐ™์€ ๋ช…๋ น์„ ํ•œ ๋ฒˆ์— ์‹คํ–‰ํ•˜๊ณ , ์ „๋ถ€์— ํŒŒ์ผ์„ ๋ณด๋‚ด๊ณ  ๊ฐ€์ ธ์˜ค๊ณ , ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ณ , OS์™€ ์ปค๋„, ์•„ํ‚คํ…์ฒ˜, ๊ฐ€๋™ ์‹œ๊ฐ„ ๋ชฉ๋ก์„ ๋ชจ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**RBAC/๊ณต์œ :** -์—ญํ• ์„ ์ƒ์„ฑํ•˜๊ณ  ์‚ฌ์šฉ์ž/์—ญํ•  ๊ฐ„์— ํ˜ธ์ŠคํŠธ๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์ธ์ฆ ์œ ํ˜•๊ณผ ๋ชจ๋“  ํ˜ธ์ŠคํŠธ ํ”„๋กœํ† ์ฝœ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. +**AI ์–ด์‹œ์Šคํ„ดํŠธ:** +์„ ํƒ ๊ธฐ๋Šฅ์ด๋ฉฐ ์ง์ ‘ ์ผœ๊ธฐ ์ „๊นŒ์ง€๋Š” ๊บผ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. OpenAI, Anthropic, Gemini, Ollama ๋˜๋Š” OpenAI ํ˜ธํ™˜ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์—ฐ๊ฒฐํ•ด ๋‚ด ํ™˜๊ฒฝ์— ๋Œ€ํ•ด ๋ฌผ์–ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜ธ์ŠคํŠธ์™€ ํ”Œ๋ฆฟ, ์Šค๋‹ˆํŽซ, ์•Œ๋ฆผ์„ ์ฝ์„ ์ˆ˜ ์žˆ์ง€๋งŒ ์ง์ ‘ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ์Šน์ธ๋ฐ›์„ ์ œ์•ˆ์œผ๋กœ ๋‚ด๋†“์Šต๋‹ˆ๋‹ค. ์ž๊ฒฉ ์ฆ๋ช…๊ณผ ์‚ฌ์šฉ์ž, ์„ค์ •์—๋Š” ์ ˆ๋Œ€ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ์ธ์Šคํ„ด์Šค ์ „์ฒด์—์„œ ๊บผ ๋‘˜ ์ˆ˜ ์žˆ๊ณ , ์ดˆ๊ธฐ ์„ค์ •์—์„œ ์•„์˜ˆ ์ˆจ๊ธธ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. -**์‹œ๋ฆฌ์–ผ ์—ฐ๊ฒฐ:** -๋ธŒ๋ผ์šฐ์ € ๋˜๋Š” ๋ฐ์Šคํฌํ†ฑ ์•ฑ์—์„œ ์ง์ ‘ ์‹œ๋ฆฌ์–ผ ์žฅ์น˜(๋ผ์šฐํ„ฐ, ์Šค์œ„์น˜, ๋งˆ์ดํฌ๋กœ์ปจํŠธ๋กค๋Ÿฌ ๋“ฑ)์— ์—ฐ๊ฒฐ. ๋ณด๋“œ๋ ˆ์ดํŠธ, ๋ฐ์ดํ„ฐ ๋น„ํŠธ, ์Šคํ†ฑ ๋น„ํŠธ, ํŒจ๋ฆฌํ‹ฐ ๊ตฌ์„ฑ. ์ง€์› ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” Web Serial API๋ฅผ, Electron ์•ฑ์—์„œ๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐฑ์—”๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +**๋กœ๊ทธ์ธ๊ณผ ์‚ฌ์šฉ์ž:** +๋กœ์ปฌ ๊ณ„์ •๊ณผ ํ•จ๊ป˜ OIDC, LDAP, GitHub, Google ๋กœ๊ทธ์ธ์„ ์ง€์›ํ•˜๊ณ  2๋‹จ๊ณ„ ์ธ์ฆ(TOTP), ํŒจ์Šคํ‚ค(WebAuthn), ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๊ธฐ๋„ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ์‚ฌ์šฉ์ž๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , OIDC ๊ทธ๋ฃน์„ ์—ญํ• ์— ์—ฐ๊ฒฐํ•˜๊ณ , ๋ชจ๋“  ํ”Œ๋žซํผ์˜ ํ™œ์„ฑ ์„ธ์…˜์„ ๋ณด๊ณ  ํ•ด์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ ๊ณ„์ •๊ณผ OIDC ๊ณ„์ •์„ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๊ณ , ๋ˆ„๊ฐ€ ๋ฌด์—‡์„ ํ–ˆ๋Š”์ง€๋Š” ๊ฐ์‚ฌ ๋กœ๊ทธ์—์„œ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. +**์—ญํ• ๊ณผ ๊ณต์œ :** +์—ญํ• ์„ ๋งŒ๋“ค๊ณ  ์—ฐ๊ฒฐ, ๋ณด๊ธฐ, ํŽธ์ง‘, ๊ด€๋ฆฌ๋ผ๋Š” ๋„ค ๋‹จ๊ณ„๋กœ ํ˜ธ์ŠคํŠธ๋ฅผ ์‚ฌ์šฉ์ž๋‚˜ ์—ญํ• ์— ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์ธ์ฆ ๋ฐฉ์‹๊ณผ ๋ชจ๋“  ํ”„๋กœํ† ์ฝœ์—์„œ ๋™์ž‘ํ•˜๋ฉฐ, ๊ณต์œ ํ•œ ํ˜ธ์ŠคํŠธ์— ์“ธ ์ž๊ฒฉ ์ฆ๋ช…์„ ๋”ฐ๋กœ ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. + + + + + + **์•Œ๋ฆผ:** -ํ˜ธ์ŠคํŠธ ๋ฉ”ํŠธ๋ฆญ(CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ ๋“ฑ)์— ๋Œ€ํ•œ ์ž„๊ณ„๊ฐ’ ๊ธฐ๋ฐ˜ ์•Œ๋ฆผ ๊ทœ์น™์„ ์„ค์ •ํ•˜๊ณ  ํŠธ๋ฆฌ๊ฑฐ๋  ๋•Œ ntfy ๋˜๋Š” ์›นํ›…์„ ํ†ตํ•ด ์•Œ๋ฆผ ์ˆ˜์‹ . ๊ธฐ๋ก ๋กœ๊ทธ์—์„œ ๋ฐœ์ƒ ์ค‘์ธ ์•Œ๋ฆผ๊ณผ ํ•ด๊ฒฐ๋œ ์•Œ๋ฆผ ํ™•์ธ. +CPU์™€ ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ ๊ฐ™์€ ํ˜ธ์ŠคํŠธ ์ง€ํ‘œ์— ๊ทœ์น™์„ ๊ฑธ์–ด ๋‘๊ณ  ์กฐ๊ฑด์ด ๊ฑธ๋ฆฌ๋ฉด ntfy๋‚˜ Discord, ์›นํ›…์œผ๋กœ ์•Œ๋ฆผ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ๋ฐœ์ƒ ์ค‘์ธ ์•Œ๋ฆผ๊ณผ ํ•ด์ œ๋œ ์•Œ๋ฆผ์„ ๊ธฐ๋ก์—์„œ ๋ณด๊ณ , ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š์„ ๊ฒƒ์€ ์ง€์›Œ ๋‘˜ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - - **ํ™ˆํŽ˜์ด์ง€:** -๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์œ„์ ฏ ๊ทธ๋ฆฌ๋“œ๋ฅผ ๊ฐ–์ถ˜ ์™„์ „ ๋งž์ถคํ˜• ํ™ˆํŽ˜์ด์ง€. ํ˜ธ์ŠคํŠธ ์ƒํƒœ, ์„œ๋น„์Šค ๋งํฌ, ์‹œ๊ณ„, ๋ฉ”๋ชจ, RSS ํ”ผ๋“œ, ๋‚ ์”จ, Docker ์ปจํ…Œ์ด๋„ˆ, ํ˜ธ์ŠคํŠธ ๋ฉ”ํŠธ๋ฆญ ์ฐจํŠธ, ์ž„๋ฒ ๋””๋“œ ํ„ฐ๋ฏธ๋„, iframe ๋“ฑ์˜ ์œ„์ ฏ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ. - - - - -**๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•”ํ˜ธํ™”:** -๋ฐฑ์—”๋“œ๊ฐ€ ์•”ํ˜ธํ™”๋œ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ๋กœ ์ €์žฅ๋จ. ์ž์„ธํ•œ ๋‚ด์šฉ์€ [๋ฌธ์„œ](https://docs.termix.site/security)๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”. +์ง์ ‘ ๊พธ๋ฏธ๋Š” ๋Œ์–ด๋‹ค ๋†“๊ธฐ ์œ„์ ฏ ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. ํ˜ธ์ŠคํŠธ ์ƒํƒœ, ํ•‘, ์„œ๋น„์Šค ๋งํฌ, ๋ถ๋งˆํฌ, ๊ฒ€์ƒ‰, ์‹œ๊ณ„, ๋‹ฌ๋ ฅ, ์นด์šดํŠธ๋‹ค์šด, ๋ฉ”๋ชจ, RSS, ๋‚ ์”จ, ์ด๋ฏธ์ง€, iframe, Docker, ํ„ฐ๋„, ์ง€ํ‘œ ์ฐจํŠธ, ์‚ฌ์šฉ์ž API, ์‹ฌ์ง€์–ด ์‚ด์•„ ์žˆ๋Š” ํ„ฐ๋ฏธ๋„๊นŒ์ง€ ์œ„์ ฏ์œผ๋กœ ๋†“์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**๋„คํŠธ์›Œํฌ ๊ทธ๋ž˜ํ”„:** -๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์‚ฌ์šฉ์ž ์ •์˜ํ•˜์—ฌ SSH ์—ฐ๊ฒฐ ๊ธฐ๋ฐ˜์˜ ํ™ˆ๋žฉ ๋„คํŠธ์›Œํฌ๋ฅผ ์ƒํƒœ ํ‘œ์‹œ์™€ ํ•จ๊ป˜ ์‹œ๊ฐํ™”. +**์Šค๋‹ˆํŽซ๊ณผ ๋„๊ตฌ:** +์ž์ฃผ ์“ฐ๋Š” ๋ช…๋ น์„ ์ €์žฅํ•ด ๋‘๊ณ  ํ•œ ๋ฒˆ์— ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ˜ธ์ŠคํŠธ ๊ฐ’์ด๋‚˜ ์ง์ ‘ ๋„ฃ๋Š” ๊ฐ’์„ ๋ณ€์ˆ˜๋กœ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ด๋ ค ์žˆ๋Š” ๋ชจ๋“  ํ„ฐ๋ฏธ๋„์—์„œ ๊ฐ™์€ ๋ช…๋ น์„ ํ•œ๊บผ๋ฒˆ์— ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ณ , ๋ช…๋ น ๊ธฐ๋ก๋„ ์ž๋™ ์™„์„ฑ์œผ๋กœ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**SSH ๋„๊ตฌ:** -ํ•œ ๋ฒˆ์˜ ํด๋ฆญ์œผ๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ ๋ช…๋ น์–ด ์Šค๋‹ˆํŽซ ์ƒ์„ฑ. ์—ฌ๋Ÿฌ ์—ด๋ฆฐ ํ„ฐ๋ฏธ๋„์—์„œ ๋™์‹œ์— ํ•˜๋‚˜์˜ ๋ช…๋ น์–ด ์‹คํ–‰. +**์„ธ์…˜ ๊ณต์œ :** +ํ„ฐ๋ฏธ๋„๊ณผ RDP, VNC, Telnet ์„ธ์…˜์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ๊ณ„์ • ์—†์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋งํฌ๋ฅผ ๋ณด๋‚ด๊ฑฐ๋‚˜ ํŠน์ • Termix ์‚ฌ์šฉ์ž์™€ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๊ณ , ๋ณด๊ธฐ๋งŒ ํ• ์ง€ ์กฐ์ž‘๊นŒ์ง€ ํ• ์ง€ ๊ณ ๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณต์œ ๋Š” ์•Œ์•„์„œ ๋งŒ๋ฃŒ๋˜๊ฒŒ ํ•˜๊ฑฐ๋‚˜ ์–ธ์ œ๋“  ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๊ณ , ์ „์ฒด ๋˜๋Š” ํ˜ธ์ŠคํŠธ๋ณ„๋กœ ๊บผ ๋‘˜ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**์ง€์† ํƒญ:** -์‚ฌ์šฉ์ž ํ”„๋กœํ•„์—์„œ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ SSH ์„ธ์…˜ ๋ฐ ํƒญ์ด ๊ธฐ๊ธฐ/์ƒˆ๋กœ ๊ณ ์นจ ๊ฐ„์— ์—ด๋ฆฐ ์ƒํƒœ ์œ ์ง€. +**์„ธ์…˜ ๋…นํ™”์™€ ๋กœ๊ทธ:** +ํ„ฐ๋ฏธ๋„๊ณผ RDP, VNC ์„ธ์…˜์„ ๋…นํ™”ํ•ด ๋‘์—ˆ๋‹ค๊ฐ€ ๋‚˜์ค‘์— ๋‹ค์‹œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ธ์…˜์˜ ํ…์ŠคํŠธ ๋กœ๊ทธ๋ฅผ ๋‚ด๋ ค๋ฐ›์„ ์ˆ˜ ์žˆ๊ณ , ์—ฐ๊ฒฐ ๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด ์—ฐ๊ฒฐํ•˜๋Š” ๋™์•ˆ ๋ฌด์Šจ ์ผ์ด ์žˆ์—ˆ๋Š”์ง€ ๊ทธ๋Œ€๋กœ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -**๋‹ค๊ตญ์–ด ์ง€์›:** -์•ฝ 30๊ฐœ ์–ธ์–ด ๋‚ด์žฅ ์ง€์›([Crowdin](https://docs.termix.site/translations)์œผ๋กœ ๊ด€๋ฆฌ). +**์‹œ๋ฆฌ์–ผ ์—ฐ๊ฒฐ:** +๋ผ์šฐํ„ฐ์™€ ์Šค์œ„์น˜, ๋งˆ์ดํฌ๋กœ์ปจํŠธ๋กค๋Ÿฌ ๊ฐ™์€ ์‹œ๋ฆฌ์–ผ ์žฅ์น˜์— ๋ธŒ๋ผ์šฐ์ €๋‚˜ ๋ฐ์Šคํฌํ†ฑ ์•ฑ์—์„œ ์ ‘์†ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณด๋“œ๋ ˆ์ดํŠธ์™€ ๋ฐ์ดํ„ฐ ๋น„ํŠธ, ์Šคํ†ฑ ๋น„ํŠธ, ํŒจ๋ฆฌํ‹ฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€์›๋˜๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” Web Serial API๋ฅผ, ๋ฐ์Šคํฌํ†ฑ ์•ฑ์—์„œ๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐฑ์—”๋“œ๋ฅผ ์”๋‹ˆ๋‹ค. + + + + + + +**Tailscale:** +tailnet์—์„œ ๊ธฐ๊ธฐ๋ฅผ ๊ฐ€์ ธ์™€ ๋ช‡ ๋ฒˆ์˜ ํด๋ฆญ์œผ๋กœ ํ˜ธ์ŠคํŠธ๋กœ ์ถ”๊ฐ€ํ•˜๊ณ , Tailscale SSH๋กœ ์ ‘์†ํ•˜๋ฉด ์ ‘๊ทผ ๊ถŒํ•œ์€ tailnet ACL์ด ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ž๊ฒฉ ์ฆ๋ช…์„ ์ €์žฅํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. Headscale๊ณผ ์‚ฌ์šฉ์ž ์ง€์ • ์—”๋“œํฌ์ธํŠธ๋„ ๋ฉ๋‹ˆ๋‹ค. + + + + +**Proxmox:** +Proxmox ์ธ์Šคํ„ด์Šค์—์„œ ํ˜ธ์ŠคํŠธ๋ฅผ ๋ฐ”๋กœ ๊ฐ€์ ธ์˜ค๊ณ , ๋…ธ๋“œ์™€ ๊ฒŒ์ŠคํŠธ์˜ CPU์™€ ๋ฉ”๋ชจ๋ฆฌ, ์Šคํ† ๋ฆฌ์ง€ ์ƒํƒœ๋ฅผ ์ „์šฉ ํƒญ์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + + + + + + +**์›Œํฌ์ŠคํŽ˜์ด์Šค์™€ ํƒญ:** +ํƒญ๊ณผ ๋ถ„ํ•  ๋ฐฐ์น˜๋ฅผ ํ†ต์งธ๋กœ ์ €์žฅํ•ด ๋‘๊ณ  ํ•œ ๋ฒˆ์— ๋‹ค์‹œ ์—ด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Termix๋Š” ๋งˆ์ง€๋ง‰ ์„ธ์…˜๋„ ๊ธฐ์–ตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒˆ๋กœ ๊ณ ์นจ์„ ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๊ธฐ๋ฅผ ๋ฐ”๊ฟ”๋„ ํƒญ์ด ๊ทธ๋Œ€๋กœ ๋Œ์•„์˜ต๋‹ˆ๋‹ค. + + + + +**์„ค์น˜ ์•ˆ๋‚ด:** +์งง์€ ์„ค์ • ๊ณผ์ •์ด ํ™”๋ฉด ํ”„๋ฆฌ์…‹๊ณผ ํ…Œ๋งˆ, ์“ฐ๊ณ  ์‹ถ์€ ๊ธฐ๋Šฅ, ์ฒซ ํ˜ธ์ŠคํŠธ๋ฅผ ๊ณ ๋ฅด๋„๋ก ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. ๊ฐ„๋‹จ ๋ชจ๋“œ๋Š” ์“ฐ์ง€ ์•Š๋Š” ๊ฒƒ์„ ์ˆจ๊ฒจ ์ฃผ๊ณ , ์„ค์ •์€ ์–ธ์ œ๋“  ๋‹ค์‹œ ํ•˜๊ฑฐ๋‚˜ ํ”„๋ฆฌ์…‹์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + + + + + + +**๋ฐ์Šคํฌํ†ฑ ๋‹จ๋… ์‹คํ–‰๊ณผ ๋™๊ธฐํ™”:** +๋ฐ์Šคํฌํ†ฑ ์•ฑ์€ ์ž์ฒด ๋ฐฑ์—”๋“œ์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ์„œ๋ฒ„ ์—†์ด ํ˜ผ์ž ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค. Termix ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋ฉด ํ˜ธ์ŠคํŠธ์™€ ์ž๊ฒฉ ์ฆ๋ช…, ์Šค๋‹ˆํŽซ ๋“ฑ์„ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๊ณ , ์—ฐ๊ฒฐ์„ ๋กœ์ปฌ์—์„œ ์‹œ์ž‘ํ• ์ง€ ์„œ๋ฒ„๋ฅผ ๊ฑฐ์น ์ง€๋„ ๊ณ ๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + + + + +**๋ช…๋ น์ค„ ๋„๊ตฌ:** +์…ธ๊ณผ ์Šคํฌ๋ฆฝํŠธ์—์„œ ์“ฐ๋Š” `termix` CLI์ž…๋‹ˆ๋‹ค. ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ , ํ˜ธ์ŠคํŠธ ํ•˜๋‚˜๋‚˜ ํ”Œ๋ฆฟ ์ „์ฒด์—์„œ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜๊ณ , SFTP๋กœ ํŒŒ์ผ์„ ์˜ฎ๊ธฐ๊ณ , ํ˜ธ์ŠคํŠธ์™€ ์Šค๋‹ˆํŽซ, ์ž๊ฒฉ ์ฆ๋ช…์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. `npm install -g @termix-cli/cli`๋กœ ์„ค์น˜ํ•˜๊ฑฐ๋‚˜ ๋‹จ๋… ์‹คํ–‰ ํŒŒ์ผ์„ ๋ฐ›์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. [CLI ๋ฌธ์„œ](https://docs.termix.site/cli)๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”. + + + + + + +**๋ณด์•ˆ:** +๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ํ‚ค๋ฅผ ๋น„๋กฏํ•œ ๋น„๋ฐ€ ์ •๋ณด๋Š” ์‚ฌ์šฉ์ž๋ณ„๋กœ ์•”ํ˜ธํ™”๋˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ ์ž์ฒด๋„ ๋””์Šคํฌ์—์„œ ์•”ํ˜ธํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€๋Š” [๋ฌธ์„œ](https://docs.termix.site/security)์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + + + + +**์–ธ์–ด:** +์•ฝ 30๊ฐœ ์–ธ์–ด๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ๋“ค์–ด ์žˆ์œผ๋ฉฐ [Crowdin](https://docs.termix.site/translations)์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. @@ -199,17 +255,20 @@ Tailscale ๋„คํŠธ์›Œํฌ์˜ ๊ธฐ๊ธฐ๋ฅผ ๋‚˜์—ดํ•˜์—ฌ ํ˜ธ์ŠคํŠธ๋กœ ๋น ๋ฅด๊ฒŒ ์ถ”๊ฐ€

๋” ๋งŽ์€ ๊ธฐ๋Šฅ
-- **๋Œ€์‹œ๋ณด๋“œ** - ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์„œ๋ฒ„ ์ •๋ณด๋ฅผ ํ•œ๋ˆˆ์— ํ™•์ธ -- **API ํ‚ค** - ์ž๋™ํ™”/CI์— ์‚ฌ์šฉํ•  ๋งŒ๋ฃŒ์ผ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž ๋ฒ”์œ„ API ํ‚ค ์ƒ์„ฑ -- **๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ/๊ฐ€์ ธ์˜ค๊ธฐ** - SSH ํ˜ธ์ŠคํŠธ, ์ž๊ฒฉ ์ฆ๋ช…, ํŒŒ์ผ ๊ด€๋ฆฌ์ž ๋ฐ์ดํ„ฐ์˜ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฐ ๊ฐ€์ ธ์˜ค๊ธฐ -- **์ž๋™ SSL ์„ค์ •** - HTTPS ๋ฆฌ๋””๋ ‰์…˜์„ ํฌํ•จํ•œ ๋‚ด์žฅ SSL ์ธ์ฆ์„œ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ -- **๋ชจ๋˜ UI** - React, Tailwind CSS, Shadcn์œผ๋กœ ๊ตฌ์ถ•๋œ ๊น”๋”ํ•œ ๋ฐ์Šคํฌํ†ฑ/๋ชจ๋ฐ”์ผ ์นœํ™”์  ์ธํ„ฐํŽ˜์ด์Šค. ๋ผ์ดํŠธ, ๋‹คํฌ, ๋“œ๋ผํ˜๋ผ ๋“ฑ ๋‹ค์–‘ํ•œ UI ํ…Œ๋งˆ ์„ ํƒ ๊ฐ€๋Šฅ. URL ๋ผ์šฐํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ์—ฐ๊ฒฐ์„ ์ „์ฒด ํ™”๋ฉด์œผ๋กœ ์—ด๊ธฐ ๊ฐ€๋Šฅ. -- **๋ช…๋ น์–ด ๊ธฐ๋ก** - ์ด์ „์— ์‹คํ–‰ํ•œ SSH ๋ช…๋ น์–ด์˜ ์ž๋™ ์™„์„ฑ ๋ฐ ์กฐํšŒ -- **๋น ๋ฅธ ์—ฐ๊ฒฐ** - ์—ฐ๊ฒฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ์„œ๋ฒ„์— ์ ‘์† -- **๋ช…๋ น์–ด ํŒ”๋ ˆํŠธ** - ์™ผ์ชฝ Shift ํ‚ค๋ฅผ ๋‘ ๋ฒˆ ๋ˆŒ๋Ÿฌ ํ‚ค๋ณด๋“œ๋กœ SSH ์—ฐ๊ฒฐ์— ๋น ๋ฅด๊ฒŒ ์ ‘๊ทผ -- **Proxmox ํ†ตํ•ฉ** - Proxmox ์ธ์Šคํ„ด์Šค์—์„œ Termix๋กœ ํ˜ธ์ŠคํŠธ๋ฅผ ์ž๋™ ์ถ”๊ฐ€ -- **ํ’๋ถ€ํ•œ SSH ๊ธฐ๋Šฅ** - ์ ํ”„ ํ˜ธ์ŠคํŠธ, Warpgate, TOTP ๊ธฐ๋ฐ˜ ์—ฐ๊ฒฐ, SOCKS5, ํ˜ธ์ŠคํŠธ ํ‚ค ๊ฒ€์ฆ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž๋™ ์ž…๋ ฅ, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, ํฌํŠธ ๋…ธํ‚น, ํ„ฐ๋ฏธ๋„ ๋กœ๊น…, SSH ์—์ด์ „ํŠธ ํฌ์›Œ๋”ฉ, Bitwarden SSH ์—์ด์ „ํŠธ, HashiCorp Vault SSH ์„œ๋ช… ๋“ฑ ์ง€์›. -- **Termix ID** - Termix์— ๋‚ด์žฅ๋œ sshid.io์™€ ๋™๋“ฑํ•œ ๊ธฐ๋Šฅ. ํ•ธ๋“ค์„ ๋“ฑ๋กํ•˜๊ณ , ๋ฆฌ์กธ๋ฒ„ URL์— ๊ณต๊ฐœ SSH ํ‚ค๋ฅผ ๊ฒŒ์‹œํ•˜๋ฉฐ, ๋‚ด์žฅ CA๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SSH ์ธ์ฆ์„œ๋ฅผ ๋ฐœ๊ธ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +- **๋Œ€์‹œ๋ณด๋“œ** - ์ง์ ‘ ๋ฐฐ์น˜ํ•œ ์นด๋“œ๋กœ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ํ•œ๋ˆˆ์— +- **๋„คํŠธ์›Œํฌ ๊ทธ๋ž˜ํ”„** - ํ˜ธ์ŠคํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ™ˆ๋žฉ์„ ๊ทธ๋ ค ์ฃผ๊ณ  ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„ ํ‘œ์‹œ +- **tmux ๋ชจ๋‹ˆํ„ฐ** - tmux ์„ธ์…˜๊ณผ ์ฐฝ, ํŽ˜์ธ์„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์™€ ๊ฒ€์ƒ‰์œผ๋กœ ์‚ดํŽด๋ณด๊ธฐ +- **API ํ‚ค** - ์Šคํฌ๋ฆฝํŠธ์™€ CI์šฉ, ๋งŒ๋ฃŒ์ผ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž๋ณ„ ํ‚ค +- **๋‚ด๋ณด๋‚ด๊ธฐ์™€ ๊ฐ€์ ธ์˜ค๊ธฐ** - ํ˜ธ์ŠคํŠธ์™€ ์ž๊ฒฉ ์ฆ๋ช…, ํŒŒ์ผ ๊ด€๋ฆฌ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฎ๊ธฐ๊ธฐ +- **์ž๋™ SSL** - ์ธ์ฆ์„œ ๋ฐœ๊ธ‰๊ณผ ๊ฐฑ์‹ , HTTPS ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ์•Œ์•„์„œ. ์ง์ ‘ ๋งŒ๋“  ์ธ์ฆ์„œ๋„ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +- **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค** - ๊ธฐ๋ณธ์€ SQLite, PostgreSQL๊ณผ MySQL๋„ ์ง€์› +- **ํ˜„๋Œ€์ ์ธ UI** - ๋ฐ์Šคํฌํ†ฑ๊ณผ ๋ชจ๋ฐ”์ผ์—์„œ ๋ชจ๋‘ ์“ธ ์ˆ˜ ์žˆ๋Š” ๊น”๋”ํ•œ React ํ™”๋ฉด. ๋ผ์ดํŠธ์™€ ๋‹คํฌ, Dracula ๊ฐ™์€ ํ…Œ๋งˆ ์ œ๊ณต. ์–ด๋–ค ์—ฐ๊ฒฐ์ด๋“  URL๋กœ ์ „์ฒด ํ™”๋ฉด์—์„œ ์—ด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +- **๋ช…๋ น ํŒ”๋ ˆํŠธ** - ์™ผ์ชฝ Shift๋ฅผ ๋‘ ๋ฒˆ ๋ˆŒ๋Ÿฌ ํ‚ค๋ณด๋“œ๋กœ ํ˜ธ์ŠคํŠธ๋กœ ์ด๋™ +- **ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค** - ํƒญ ์ด๋™๊ณผ ๋‹ซ๊ธฐ ๋“ฑ, ๋ชจ๋‘ ๋‹ค์‹œ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +- **Wake-on-LAN** - Termix์—์„œ๋„, ์ž๋™ํ™” ๋‹จ๊ณ„์—์„œ๋„ ์ปดํ“จํ„ฐ๋ฅผ ์ผœ๊ธฐ +- **์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ํ”„๋ก์‹œ ์ธ์ฆ** - ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๊ฐ€ ๋กœ๊ทธ์ธ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋„˜๊ฒจ์ฃผ๊ธฐ +- **ํ’๋ถ€ํ•œ SSH ๊ธฐ๋Šฅ** - ์ ํ”„ ํ˜ธ์ŠคํŠธ, Warpgate, TOTP ์ž…๋ ฅ, SOCKS5, ํ˜ธ์ŠคํŠธ ํ‚ค ํ™•์ธ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž๋™ ์ž…๋ ฅ, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, ํฌํŠธ ๋…ธํ‚น, ํ„ฐ๋ฏธ๋„ ๋กœ๊ทธ, ์—์ด์ „ํŠธ ํฌ์›Œ๋”ฉ, Bitwarden SSH ์—์ด์ „ํŠธ, HashiCorp Vault SSH ์„œ๋ช… ๋“ฑ +- **Termix ID** - sshid.io ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๋‚ด์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ธ๋“ค์„ ๋“ฑ๋กํ•˜๊ณ  ๋ฆฌ์กธ๋ฒ„ URL์— ๊ณต๊ฐœ ํ‚ค๋ฅผ ์˜ฌ๋ฆฌ๊ณ  ๋‚ด์žฅ CA์—์„œ SSH ์ธ์ฆ์„œ๋ฅผ ๋ฐœ๊ธ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค @@ -220,15 +279,15 @@ Tailscale ๋„คํŠธ์›Œํฌ์˜ ๊ธฐ๊ธฐ๋ฅผ ๋‚˜์—ดํ•˜์—ฌ ํ˜ธ์ŠคํŠธ๋กœ ๋น ๋ฅด๊ฒŒ ์ถ”๊ฐ€ - + - + - + @@ -252,9 +311,9 @@ Tailscale ๋„คํŠธ์›Œํฌ์˜ ๊ธฐ๊ธฐ๋ฅผ ๋‚˜์—ดํ•˜์—ฌ ํ˜ธ์ŠคํŠธ๋กœ ๋น ๋ฅด๊ฒŒ ์ถ”๊ฐ€ ## ์„ค์น˜ -๋ชจ๋“  ํ”Œ๋žซํผ์— Termix๋ฅผ ์„ค์น˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ Termix [๋ฌธ์„œ](https://docs.termix.site/install)๋ฅผ ๋ฐฉ๋ฌธํ•˜์„ธ์š”. +๋ชจ๋“  ํ”Œ๋žซํผ์˜ ์ž์„ธํ•œ ์„ค์น˜ ๋ฐฉ๋ฒ•์€ [Termix ๋ฌธ์„œ](https://docs.termix.site/install)๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”. -๋‹ค์Œ์€ Docker Compose ํŒŒ์ผ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค(์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ๊ณ„ํš์ด ์—†๋‹ค๋ฉด guacd์™€ ๋„คํŠธ์›Œํฌ๋ฅผ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค): +Docker Compose ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค(์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ ๊ธฐ๋Šฅ์„ ์“ฐ์ง€ ์•Š๋Š”๋‹ค๋ฉด `guacd`์™€ ๋„คํŠธ์›Œํฌ ๋ถ€๋ถ„์€ ๋นผ๋„ ๋ฉ๋‹ˆ๋‹ค): ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### ๋ช…๋ น์ค„ ๋„๊ตฌ + +Termix์—๋Š” CLI๋„ ์žˆ์–ด์„œ ํ„ฐ๋ฏธ๋„์—์„œ ์„œ๋ฒ„๋ฅผ ๊ด€๋ฆฌํ•˜๊ฑฐ๋‚˜ Termix๋ฅผ ์ž๊ธฐ ์Šคํฌ๋ฆฝํŠธ์— ๋„ฃ์–ด ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ , ํ˜ธ์ŠคํŠธ ํ•˜๋‚˜๋‚˜ ํ”Œ๋ฆฟ ์ „์ฒด์—์„œ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜๊ณ , SFTP๋กœ ํŒŒ์ผ์„ ์˜ฎ๊ธฐ๊ณ , ํ˜ธ์ŠคํŠธ์™€ ์Šค๋‹ˆํŽซ, ์ž๊ฒฉ ์ฆ๋ช…์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ „์ฒด ๋ฌธ์„œ๋Š” [docs.termix.site/cli](https://docs.termix.site/cli)์— ์žˆ์Šต๋‹ˆ๋‹ค. + +### ํด๋ผ์šฐ๋“œ ํ˜ธ์ŠคํŒ… + +Termix ์„œ๋ฒ„๋Š” ์ง‘ ์•ˆ ๋„คํŠธ์›Œํฌ๊ฐ€ ์•„๋‹ˆ๋ผ VPS์—์„œ ๋Œ๋ฆด ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ ๋Œ€์ƒ ๋„คํŠธ์›Œํฌ ์œ„์—์„œ ๋Œ์•„๊ฐ€๋ฉด ์žฅ์• ๊ฐ€ ๋‚ฌ์„ ๋•Œ Termix๋„ ๊ฐ™์ด ๋ฉˆ์ถฐ์„œ, ์ •์ž‘ ๊ณ ์ณ์•ผ ํ•  ๋•Œ ์“ธ ์ˆ˜ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ฐ–์—์„œ ๋Œ๋ฆฌ๋ฉด ์–ธ์ œ๋“  ์ ‘์†ํ•  ์ˆ˜ ์žˆ๊ณ  ๊ณ ์ • IP๋„ ์ƒ๊ธฐ๋ฉฐ, VPN์ด๋‚˜ ํฌํŠธ ํฌ์›Œ๋”ฉ ์—†์ด ์–ด๋””์„œ๋‚˜ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +[GINERNET](https://docs.termix.site/install/ginernet)์€ Termix๋ฅผ ํ›„์›ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋ฌธ์„œ์— ์ด ํšŒ์‚ฌ VPS์— ๋ฐฐํฌํ•˜๋Š” ๋‹จ๊ณ„๋ณ„ ์•ˆ๋‚ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. + +
+ +## ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ + +Termix๋Š” ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ ์ต๋ช…์˜ ์ž‘์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ์ธ์Šคํ„ด์Šค๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋Œ์•„๊ฐ€๋Š”์ง€, ์–ด๋–ค ๊ธฐ๋Šฅ์ด ์“ฐ์ด๋Š”์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ๋ฌด์ž‘์œ„ ์ธ์Šคํ„ด์Šค ID, ์‚ฌ์šฉ์ž์™€ ํ˜ธ์ŠคํŠธ ์ˆ˜, ์•ฑ ๋ฒ„์ „, ์ตœ๊ทผ 24์‹œ๊ฐ„ ๋™์•ˆ ์“ด ๊ธฐ๋Šฅ(ํ„ฐ๋ฏธ๋„, ํŒŒ์ผ ๊ด€๋ฆฌ์ž, ํ„ฐ๋„, Docker ๋“ฑ)๋งŒ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ํ˜ธ์ŠคํŠธ ์ด๋ฆ„, IP ์ฃผ์†Œ, ์ž๊ฒฉ ์ฆ๋ช…์ฒ˜๋Ÿผ ๋‚˜๋‚˜ ๋‚ด ์„œ๋ฒ„๋ฅผ ์•Œ์•„๋ณผ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ์ „ํ˜€ ๋‹ด๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +๊ธฐ๋ณธ์œผ๋กœ ์ผœ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ ์„ค์ •์˜ ์ผ๋ฐ˜์—์„œ ๋„๊ฑฐ๋‚˜, Termix๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— `ENABLE_TELEMETRY=false`๋ฅผ ์ง€์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. +
## ํ›„์› -Termix๋Š” ๊ตฌ๋…์ด๋‚˜ ์œ ๋ฃŒ ์š”๊ธˆ์ œ๊ฐ€ ์—†๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ์„œ๋ฒ„ ๋น„์šฉ, ๋„๋ฉ”์ธ, ๊ฐœ๋ฐœ ์‹œ๊ฐ„์„ ์œ„ํ•ด ํ›„์›์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”. ํ›„์›์€ SAML, Kubernetes, ์—์ด์ „ํŠธ ์ง€์›๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ์‚ฌํ•ญ์„ ์—ฐ๊ตฌํ•˜๊ณ  ํ•™์Šตํ•˜๋Š” ์‹œ๊ฐ„์—๋„ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ ์ง„ํ–‰ ์ƒํ™ฉ์„ ํ™•์ธํ•˜๊ณ  ํ›„์›ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +Termix๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ ์†Œ์Šค์ด๊ณ  ๊ตฌ๋…์ด๋‚˜ ์œ ๋ฃŒ ์š”๊ธˆ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์œ ์šฉํ•˜๊ฒŒ ์“ฐ๊ณ  ๊ณ„์‹œ๋‹ค๋ฉด ์„œ๋ฒ„ ๋น„์šฉ๊ณผ ๋„๋ฉ”์ธ, ๊ฐœ๋ฐœ ์‹œ๊ฐ„์— ๋ณดํƒฌ์ด ๋˜๋„๋ก ํ›„์›์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”. ํ›„์›์€ SAML๊ณผ Kubernetes, ์—์ด์ „ํŠธ ์ง€์› ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์•Œ์•„๋ณด๊ณ  ๋ฐฐ์šฐ๋Š” ์‹œ๊ฐ„์—๋„ ์“ฐ์ž…๋‹ˆ๋‹ค. ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ณด๊ณ  ํ›„์›ํ•˜์‹œ๋ ค๋ฉด ์•„๋ž˜๋ฅผ ๋ˆŒ๋Ÿฌ ์ฃผ์„ธ์š”. [ํ›„์›ํ•˜๊ธฐ](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix๋Š” ๊ตฌ๋…์ด๋‚˜ ์œ ๋ฃŒ ์š”๊ธˆ์ œ๊ฐ€ ์—†๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์  ## ์Šคํฐ์„œ -๊ฐœ๋ฐœ ์ง€์›์„ ์œ„ํ•œ ์œ ๋ฃŒ ๊ด‘๊ณ ์— ๊ด€์‹ฌ์ด ์žˆ์œผ์‹ ๊ฐ€์š”? [mail@termix.site](mailto:mail@termix.site)๋กœ ์ด๋ฉ”์ผ์„ ๋ณด๋‚ด์ฃผ์„ธ์š”. +์œ ๋ฃŒ ๊ฒŒ์žฌ๋กœ ๊ฐœ๋ฐœ์„ ์ง€์›ํ•˜๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”? [mail@termix.site](mailto:mail@termix.site)๋กœ ๋ฉ”์ผ์„ ๋ณด๋‚ด ์ฃผ์„ธ์š”.
@@ -325,10 +410,6 @@ Termix๋Š” ๊ตฌ๋…์ด๋‚˜ ์œ ๋ฃŒ ์š”๊ธˆ์ œ๊ฐ€ ์—†๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์  Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Termix๋Š” ๊ตฌ๋…์ด๋‚˜ ์œ ๋ฃŒ ์š”๊ธˆ์ œ๊ฐ€ ์—†๋Š” ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์  Rack Genius - +    + + Ginernet +

## ์ง€์› -Termix์— ๋Œ€ํ•œ ๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๋Šฅ์„ ์š”์ฒญํ•˜๋ ค๋ฉด [Issues](https://github.com/Termix-SSH/Support/issues) ํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋กœ๊ทธ์ธํ•˜๊ณ  `New Issue`๋ฅผ ๋ˆ„๋ฅด์„ธ์š”. ์ด์Šˆ๋Š” ๊ฐ€๋Šฅํ•œ ํ•œ ์ƒ์„ธํ•˜๊ฒŒ ์ž‘์„ฑํ•˜๊ณ , ์˜์–ด๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. [Discord](https://discord.gg/jVQGdvHDrf) ์„œ๋ฒ„์— ์ฐธ์—ฌํ•˜์—ฌ ์ง€์› ์ฑ„๋„์„ ์ด์šฉํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ์‘๋‹ต ์‹œ๊ฐ„์ด ๋” ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๋Šฅ์„ ์ œ์•ˆํ•˜๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”? [์ƒˆ ์ด์Šˆ](https://github.com/Termix-SSH/Support/issues)๋ฅผ ์˜ฌ๋ฆฌ๋ฉด์„œ ๋˜๋„๋ก ์ž์„ธํžˆ, ๊ฐ€๋Šฅํ•˜๋ฉด ์˜์–ด๋กœ ์ ์–ด ์ฃผ์„ธ์š”. [Discord](https://discord.gg/jVQGdvHDrf) ์ง€์› ์ฑ„๋„์—์„œ ๋ฌผ์–ด๋ด๋„ ๋˜์ง€๋งŒ ๋‹ต๋ณ€์ด ๋Šฆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@@ -359,7 +443,7 @@ Termix์— ๋Œ€ํ•œ ๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๋Šฅ์„ ์š”์ฒญํ•˜๋ ค๋ฉด [Issues](ht [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -YouTube์—์„œ ์—…๋ฐ์ดํŠธ ๊ฐœ์š” ์‹œ์ฒญํ•˜๊ธฐ +YouTube์—์„œ ์—…๋ฐ์ดํŠธ ์†Œ๊ฐœ ๋ณด๊ธฐ

@@ -399,7 +483,7 @@ Termix์— ๋Œ€ํ•œ ๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๋Šฅ์„ ์š”์ฒญํ•˜๋ ค๋ฉด [Issues](ht
ํ”Œ๋žซํผ๋ฐฐํฌํŒ๋ฐฐํฌ ํ˜•ํƒœ
Web๋ชจ๋“  ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €(Chrome, Safari, Firefox) ยท PWA ์ง€์›์ตœ์‹  ๋ธŒ๋ผ์šฐ์ € ์ „๋ฐ˜(Chrome, Safari, Firefox) ยท PWA ์ง€์›
Windows x64/ia32ํฌํ„ฐ๋ธ” ยท MSI ์„ค์น˜ ํ”„๋กœ๊ทธ๋žจ ยท Chocolateyํฌํ„ฐ๋ธ” ยท MSI ์„ค์น˜ ํŒŒ์ผ ยท Chocolatey
Linux x64/ia32
-์ผ๋ถ€ ๋น„๋””์˜ค ๋ฐ ์ด๋ฏธ์ง€๋Š” ์ตœ์‹ ์ด ์•„๋‹ˆ๊ฑฐ๋‚˜ ๊ธฐ๋Šฅ์„ ์™„๋ฒฝํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +์ผ๋ถ€ ์˜์ƒ๊ณผ ์ด๋ฏธ์ง€๋Š” ์˜ค๋ž˜๋˜์—ˆ๊ฑฐ๋‚˜ ๊ธฐ๋Šฅ์„ ์ œ๋Œ€๋กœ ๋ณด์—ฌ ์ฃผ์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. @@ -407,10 +491,10 @@ Termix์— ๋Œ€ํ•œ ๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๋Šฅ์„ ์š”์ฒญํ•˜๋ ค๋ฉด [Issues](ht ## ๊ณ„ํš๋œ ๊ธฐ๋Šฅ -๋ชจ๋“  ๊ณ„ํš๋œ ๊ธฐ๋Šฅ์€ [Projects](https://github.com/orgs/Termix-SSH/projects/5)๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”. ๊ธฐ์—ฌ๋ฅผ ์›ํ•˜์‹œ๋ฉด [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)์„ ์ฐธ์กฐํ•˜์„ธ์š”. +๊ณ„ํš๋œ ๊ธฐ๋Šฅ์€ ๋ชจ๋‘ [Projects](https://github.com/orgs/Termix-SSH/projects/5)์— ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์—ฌํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด [๊ธฐ์—ฌ ์•ˆ๋‚ด](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)๋ฅผ ๋ด ์ฃผ์„ธ์š”.
## ๋ผ์ด์„ ์Šค -Apache License Version 2.0์— ๋”ฐ๋ผ ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ `LICENSE`๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”. +Apache License 2.0์— ๋”ฐ๋ผ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ `LICENSE`๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”. diff --git a/docs/readme/README-PT.md b/docs/readme/README-PT.md index c46606c..99884a3 100644 --- a/docs/readme/README-PT.md +++ b/docs/readme/README-PT.md @@ -4,7 +4,7 @@

Termix

-

Gerenciamento SSH auto-hospedado e acesso a area de trabalho remota

+

Gestรฃo de servidores auto-hospedada, do SSH e do desktop remoto ร s automaรงรตes

English ยท @@ -37,7 +37,7 @@
-Termix รฉ gratuito e de cรณdigo aberto. Se o achar รบtil, considere [doar](https://donate.termix.site/) para ajudar a cobrir os custos de servidor e o tempo de desenvolvimento. +O Termix รฉ gratuito e de cรณdigo aberto. Se ele te ajuda, considera [doar](https://donate.termix.site/) para ajudar a cobrir os custos de servidor e o tempo de desenvolvimento.
@@ -56,9 +56,9 @@ Termix รฉ gratuito e de cรณdigo aberto. Se o achar รบtil, considere [doar](https
-## Visao Geral +## Visรฃo geral -Termix e uma plataforma de gerenciamento de servidores tudo-em-um, de codigo aberto, sempre gratuita e auto-hospedada. Ela fornece uma solucao multiplataforma para gerenciar seus servidores e infraestrutura atraves de uma interface unica e intuitiva. Termix oferece acesso a terminal SSH, controle de desktop remoto (RDP, VNC, Telnet), capacidades de tunelamento SSH, gerenciamento remoto de arquivos SSH e muitas outras ferramentas. Termix e a alternativa perfeita, gratuita e auto-hospedada ao Termius, disponivel para todas as plataformas. +O Termix รฉ uma plataforma gratuita, de cรณdigo aberto e auto-hospedada para gerenciar os teus servidores. Ele junta num sรณ lugar terminais SSH, desktops remotos (RDP, VNC, Telnet), transferรชncia de arquivos, tรบneis, Docker, mรฉtricas e automaรงรตes, na web, no desktop e no celular. ร‰ uma alternativa auto-hospedada ao Termius que continua gratuita para sempre.
@@ -68,126 +68,182 @@ Termix e uma plataforma de gerenciamento de servidores tudo-em-um, de codigo abe -**Acesso ao Terminal SSH:** -Terminal completo com suporte a tela dividida (ate 4 paineis) com um sistema de abas similar ao navegador. Inclui suporte para personalizacao do terminal incluindo temas comuns de terminal, fontes e outros componentes. +**Terminal SSH:** +Um terminal completo com abas como as do navegador e tela dividida, atรฉ 6 painรฉis ao mesmo tempo. Escolhe o teu tema, a fonte e as cores. Acima de cada sessรฃo hรก uma barra com CPU, memรณria e disco ao vivo, alรฉm de atalhos para os arquivos, o Docker, os tรบneis e as mรฉtricas daquele host. -**Acesso a Area de Trabalho Remota:** -Suporte a RDP, VNC e Telnet pelo navegador com personalizacao completa e tela dividida. +**Desktop remoto:** +RDP, VNC e Telnet no navegador, em abas e tela dividida como qualquer outra sessรฃo. Inclui um navegador de arquivos para as unidades RDP e envio arrastando e soltando. No desktop Windows tambรฉm dรก para abrir um host no cliente RDP nativo. -**Gerenciamento de Tuneis SSH:** -Crie e gerencie tuneis SSH de servidor para servidor com reconexao automatica, monitoramento de saude e encaminhamento local, remoto ou SOCKS dinamico. As configuracoes de tunel de cliente desktop para servidor sao armazenadas localmente por instalacao de desktop, snapshots de predefinicoes C2S opcionais podem ser salvos no servidor, renomeados, carregados ou excluidos quando voce quiser mover uma configuracao de tunel local entre clientes. +**Tรบneis SSH:** +Encaminhamento local, remoto e SOCKS dinรขmico, com reconexรฃo automรกtica e verificaรงรฃo de estado. Os tรบneis de cliente para servidor do aplicativo de desktop ficam naquela mรกquina, e dรก para salvar predefiniรงรตes no servidor para levar uma configuraรงรฃo para outro computador. -**Gerenciador Remoto de Arquivos:** -Gerencie arquivos diretamente em servidores remotos com suporte para visualizar e editar codigo, imagens, audio e video. Faca upload, download, renomeie, exclua e mova arquivos facilmente com suporte sudo. Inclui suporte para mover arquivos de servidor para servidor. +**Gerenciador de arquivos:** +Navega, edita, envia, baixa, renomeia, move e apaga arquivos por SFTP, com suporte a sudo. Vรช e edita cรณdigo, imagens, รกudio e vรญdeo. Copia arquivos direto de um servidor para outro, com o caminho mais rรกpido escolhido para ti e a integridade das transferรชncias verificada. -**Gerenciamento de Docker e Podman:** -Inicie, pare, pause, remova conteineres. Visualize estatisticas de conteineres. Controle conteineres usando o terminal Docker Exec. Suporta Docker e Podman como ambiente de execucao de conteineres. Nao foi feito para substituir Portainer ou Dockge, mas sim para simplesmente gerenciar seus conteineres em vez de cria-los. +**Docker e Podman:** +Inicia, para, pausa e remove containers, acompanha as estatรญsticas e abre um shell dentro de um deles. Funciona tanto com Docker quanto com Podman. Nรฃo รฉ para substituir o Portainer ou o Dockge, sรณ para gerenciar os containers que jรก tens. -**Gerenciador de Hosts SSH:** -Salve, organize e gerencie suas conexoes SSH com tags e pastas (com personalizacao de pastas e suporte a pastas aninhadas), e salve facilmente informacoes de login reutilizaveis com a capacidade de automatizar a implantacao de chaves SSH. +**Gerenciador de hosts:** +Salva e organiza hosts com etiquetas e pastas aninhadas que podes nomear e colorir. Reaproveita credenciais salvas entre hosts, distribui chaves SSH automaticamente, agrupa hosts sob um host pai, edita e exporta em lote, e usa a conexรฃo rรกpida para conexรตes pontuais que nรฃo queres guardar. -**Metricas do Host:** -Visualize o uso de CPU, memoria e disco, rede, tempo de atividade, informacoes do sistema, firewall, monitor de portas, visualizador de logs, usuarios/permissoes, certificados e muito mais na maioria dos servidores baseados em Linux. Inclui graficos de historico em serie temporal e alertas baseados em limites com suporte a ntfy e webhook. +**Mรฉtricas de host:** +CPU, memรณria, disco, rede, temperatura, tempo ligado, processos, portas, logins e informaรงรตes do sistema na maioria dos servidores Linux, com grรกficos de histรณrico. Os cartรตes de gerenciamento deixam cuidar de serviรงos, tarefas cron, pacotes, usuรกrios, regras de firewall, WireGuard, Tailscale, certificados SSL, logs e verificaรงรตes de saรบde sem sair do Termix. -**Autenticacao de Usuarios:** -Gerenciamento seguro de usuarios com controles de administrador (podem editar informacoes de outros usuarios) e suporte para OIDC/LDAP/SSO (com controle de acesso), 2FA (TOTP) e passkey (WebAuthn). Visualize sessoes ativas de usuarios em todas as plataformas e revogue permissoes. Vincule suas contas OIDC/Locais entre si. Visualize o log de auditoria de todas as acoes dos usuarios. +**Automaรงรตes:** +Escolhe um gatilho e depois diz o que deve acontecer. Os gatilhos incluem uma mรฉtrica passando de um limite, um host caindo ou voltando, uma verificaรงรฃo de saรบde mudando, um agendamento, um evento de container ou um webhook recebido. Os passos podem rodar comandos e trechos, controlar containers e tรบneis, acordar um host, chamar uma URL, esperar, seguir por uma condiรงรฃo, rodar outra automaรงรฃo e te avisar por ntfy, Discord ou webhook. As execuรงรตes de teste deixam experimentar com seguranรงa primeiro. -**Integracao com Tailscale:** -Liste dispositivos da sua rede Tailscale para adicionรก-los rapidamente como hosts, e conecte-se usando Tailscale SSH como metodo de autenticacao, deixando as ACLs da sua rede gerenciar a autorizacao sem armazenar credenciais. +**Frotas:** +Junta hosts numa frota escolhendo um a um ou com regras de etiquetas, para que os novos entrem sozinhos. Roda um comando em todos os hosts de uma vez, envia e busca arquivos em todos eles, instala pacotes e reรบne um inventรกrio do sistema, do kernel, da arquitetura e do tempo ligado. -**RBAC/Compartilhamento:** -Crie funcoes e compartilhe hosts entre usuarios/funcoes. Suporta todos os tipos de autenticacao e todos os protocolos de host. +**Assistente de IA:** +ร‰ opcional e fica desligado atรฉ tu ligares. Conecta OpenAI, Anthropic, Gemini, Ollama ou qualquer endpoint compatรญvel com OpenAI e pergunta sobre a tua instalaรงรฃo. Ele lรช hosts, frotas, trechos e alertas, e propรตe mudanรงas para tu aprovares em vez de fazer sozinho. Nunca consegue mexer em credenciais, usuรกrios ou configuraรงรตes. Os administradores podem deixar desligado para toda a instรขncia, e dรก para escondรช-lo jรก na configuraรงรฃo inicial. -**Conexoes Seriais:** -Conecte-se a dispositivos seriais (roteadores, switches, microcontroladores, etc.) diretamente do navegador ou do aplicativo desktop. Configure taxa de baud, bits de dados, bits de parada e paridade. Usa a Web Serial API em navegadores suportados ou um backend nativo no aplicativo Electron. +**Login e usuรกrios:** +Contas locais alรฉm de login por OIDC, LDAP, GitHub e Google, com dois fatores (TOTP), chaves de acesso (WebAuthn) e dispositivos confiรกveis. Os administradores podem gerenciar usuรกrios, ligar grupos do OIDC a papรฉis, ver todas as sessรตes ativas em qualquer plataforma e encerrรก-las. Liga a tua conta local com a do OIDC e consulta o registro de auditoria do que cada um fez. +**Papรฉis e compartilhamento:** +Cria papรฉis e compartilha hosts com usuรกrios ou papรฉis em quatro nรญveis: conectar, ver, editar e gerenciar. Funciona com todos os tipos de autenticaรงรฃo e todos os protocolos, e dรก para trocar as credenciais usadas num host compartilhado. + + + + + + **Alertas:** -Defina regras de alerta baseadas em limites para metricas do host (CPU, memoria, disco, etc.) e receba notificacoes via ntfy ou webhooks quando forem ativadas. Visualize alertas ativos e resolvidos em um historico de registros. +Define regras em mรฉtricas de host como CPU, memรณria e disco, e recebe aviso por ntfy, Discord ou webhook quando elas disparam. Vรช os alertas ativos e resolvidos num histรณrico e dispensa os que nรฃo te interessam. + + + + +**Pรกgina inicial:** +Uma grade de widgets que tu mesmo montas arrastando e soltando. Tem widget para status de host, ping, links de serviรงos, favoritos, busca, relรณgios, calendรกrios, contagens regressivas, notas, RSS, previsรฃo do tempo, imagens, iframes, Docker, tรบneis, grรกficos de mรฉtricas, APIs prรณprias e atรฉ um terminal ao vivo. -**Pagina Inicial:** -Uma pagina inicial totalmente personalizavel com uma grade de widgets de arrastar e soltar. Adicione widgets para status do host, links de servicos, relogios, notas, feeds RSS, clima, conteineres Docker, graficos de metricas do host, terminais incorporados, iframes e mais. +**Trechos e ferramentas:** +Salva os comandos que usas sempre e dispara com um clique, com variรกveis para o host e para o que tu digitares. Roda um mesmo comando em todos os terminais abertos e pesquisa o teu histรณrico com preenchimento automรกtico. -**Criptografia de Banco de Dados:** -Backend armazenado como arquivos de banco de dados SQLite criptografados. Consulte a [documentacao](https://docs.termix.site/security) para mais informacoes. +**Compartilhar sessรฃo:** +Compartilha ao vivo uma sessรฃo de terminal, RDP, VNC ou Telnet. Manda um link que qualquer um entra sem conta, ou compartilha com um usuรกrio especรญfico do Termix, em somente leitura ou com escrita. Os compartilhamentos podem expirar sozinhos ou ser revogados, e dรก para desligar tudo de uma vez ou por host. -**Grafico de Rede:** -Personalize seu Dashboard para visualizar seu homelab baseado nas suas conexoes SSH com suporte de status. +**Gravaรงรฃo e registros de sessรฃo:** +Grava sessรตes de terminal, RDP e VNC e reproduz depois. Baixa registros de texto de uma sessรฃo e olha o registro de conexรฃo para ver exatamente o que aconteceu durante ela. -**Ferramentas SSH:** -Crie trechos de comandos reutilizaveis que sao executados com um unico clique. Execute um comando simultaneamente em multiplos terminais abertos. +**Conexรตes seriais:** +Fala com dispositivos seriais como roteadores, switches e microcontroladores pelo navegador ou pelo aplicativo de desktop. Define taxa de transmissรฃo, bits de dados, bits de parada e paridade. Usa a API Web Serial nos navegadores compatรญveis, ou um backend nativo no aplicativo de desktop. -**Abas Persistentes:** -Sessoes SSH e abas permanecem abertas entre dispositivos/atualizacoes se habilitado no perfil do usuario. +**Tailscale:** +Traz dispositivos da tua tailnet para adicionar como hosts em poucos cliques, e conecta com Tailscale SSH para que as ACLs da tailnet cuidem do acesso sem guardar credenciais. Headscale e endpoints personalizados tambรฉm funcionam. + + + + +**Proxmox:** +Importa hosts direto de uma instรขncia Proxmox e acompanha as estatรญsticas de nรณs e convidados, incluindo CPU, memรณria e armazenamento, numa aba prรณpria. + + + + + + +**รreas de trabalho e abas:** +Salva um conjunto de abas com a divisรฃo da tela e reabre tudo com um clique. O Termix tambรฉm lembra da tua รบltima sessรฃo, entรฃo as abas voltam depois de recarregar e em outros dispositivos. + + + + +**Configuraรงรฃo guiada:** +Uma configuraรงรฃo curta te leva por escolher uma predefiniรงรฃo de interface, o tema, as funcionalidades que queres e o teu primeiro host. O modo simples esconde o que nรฃo usas, e dรก para refazer a configuraรงรฃo ou trocar de predefiniรงรฃo quando quiseres. + + + + + + +**Desktop independente e sincronizaรงรฃo:** +O aplicativo de desktop roda sozinho, com backend e banco de dados locais, sem servidor. Tambรฉm dรก para ligar num servidor Termix e sincronizar nos dois sentidos hosts, credenciais, trechos e mais, e escolher se as conexรตes saem da tua mรกquina ou passam pelo servidor. + + + + +**Linha de comando:** +Um CLI `termix` para o teu shell e os teus scripts. Abre terminais, roda um comando num host ou numa frota inteira, move arquivos por SFTP e gerencia hosts, trechos e credenciais. Instala com `npm install -g @termix-cli/cli` ou pega um binรกrio independente. Vรช a [documentaรงรฃo do CLI](https://docs.termix.site/cli). + + + + + + +**Seguranรงa:** +Senhas, chaves e outros segredos sรฃo criptografados por usuรกrio, e os prรณprios arquivos do banco de dados podem ser criptografados em disco. Vรช a [documentaรงรฃo](https://docs.termix.site/security) para entender como funciona. **Idiomas:** -Suporte integrado para aproximadamente 30 idiomas (gerenciado pelo [Crowdin](https://docs.termix.site/translations)). +Cerca de 30 idiomas incluรญdos, gerenciados pelo [Crowdin](https://docs.termix.site/translations). @@ -199,40 +255,43 @@ Suporte integrado para aproximadamente 30 idiomas (gerenciado pelo [Crowdin](htt

Mais funcionalidades
-- **Dashboard** - Visualize informacoes do servidor de relance no seu dashboard -- **Chaves de API** - Crie chaves de API com escopo de usuario e datas de expiracao para uso em automacao/CI -- **Exportacao/Importacao de Dados** - Exporte e importe hosts SSH, credenciais e dados do gerenciador de arquivos -- **Configuracao Automatica de SSL** - Geracao e gerenciamento integrado de certificados SSL com redirecionamentos HTTPS -- **Interface Moderna** - Interface limpa compativel com desktop/mobile construida com React, Tailwind CSS e Shadcn. Escolha entre muitos temas de interface diferentes, incluindo claro, escuro, Dracula, etc. Use rotas de URL para abrir qualquer conexao em tela cheia. -- **Historico de Comandos** - Autocompletar e visualizar comandos SSH executados anteriormente -- **Conexao Rapida** - Conecte-se a um servidor sem precisar salvar os dados de conexao -- **Paleta de Comandos** - Pressione duas vezes a tecla Shift esquerda para acessar rapidamente as conexoes SSH com seu teclado -- **Integracao com Proxmox** - Adicione automaticamente hosts ao Termix a partir da sua instancia Proxmox -- **SSH Rico em Funcionalidades** - Suporta jump hosts, Warpgate, conexoes baseadas em TOTP, SOCKS5, verificacao de chave do host, preenchimento automatico de senhas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, registro de terminal, encaminhamento de agente SSH, agente SSH do Bitwarden, assinatura SSH do HashiCorp Vault, e mais. -- **Termix ID** - Um equivalente ao sshid.io integrado ao Termix. Reivindique um identificador, publique suas chaves SSH publicas em uma URL de resolucao e use uma CA integrada para emitir certificados SSH. +- **Painel** - Os teus servidores num relance, com cartรตes que tu mesmo organizas +- **Grรกfico de rede** - O teu homelab desenhado a partir dos teus hosts, com status ao vivo +- **Monitor tmux** - Percorre sessรตes, janelas e painรฉis do tmux, com prรฉvia e busca +- **Chaves de API** - Chaves por usuรกrio com data de validade para scripts e CI +- **Exportar e importar** - Leva e traz hosts, credenciais e dados do gerenciador de arquivos +- **SSL automรกtico** - Certificados gerados e renovados para ti, com redirecionamento para HTTPS, ou usa os teus +- **Bancos de dados** - SQLite por padrรฃo, com PostgreSQL e MySQL tambรฉm suportados +- **Interface moderna** - Interface React limpa que funciona no desktop e no celular, com temas como claro, escuro e Dracula. Qualquer conexรฃo abre em tela cheia por uma URL +- **Paleta de comandos** - Toca duas vezes no Shift esquerdo para ir a um host pelo teclado +- **Atalhos de teclado** - Trocar de aba, fechar abas e mais, tudo remapeรกvel +- **Wake-on-LAN** - Liga uma mรกquina pelo Termix ou por um passo de automaรงรฃo +- **Autenticaรงรฃo por proxy confiรกvel** - Deixa um proxy reverso cuidar do login e repassar o usuรกrio +- **SSH bem completo** - Hosts de salto, Warpgate, pedidos de TOTP, SOCKS5, verificaรงรฃo de chave de host, preenchimento automรกtico de senha, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, registro do terminal, encaminhamento de agente, agente SSH do Bitwarden, assinatura SSH com HashiCorp Vault e mais +- **Termix ID** - Uma versรฃo embutida do sshid.io. Registra um identificador, publica as tuas chaves pรบblicas numa URL de resoluรงรฃo e emite certificados SSH pela CA embutida
-## Suporte a Plataformas +## Plataformas suportadas - + - + - + - + @@ -250,11 +309,11 @@ Suporte integrado para aproximadamente 30 idiomas (gerenciado pelo [Crowdin](htt
-## Instalacao +## Instalaรงรฃo -Visite a [documentacao](https://docs.termix.site/install) do Termix para instrucoes completas de instalacao em todas as plataformas. +Vรช a [documentaรงรฃo do Termix](https://docs.termix.site/install) para as instruรงรตes completas de instalaรงรฃo em todas as plataformas. -Arquivo Docker Compose de exemplo (voce pode omitir o `guacd` e a rede se nao planeja usar recursos de area de trabalho remota): +Exemplo de arquivo Docker Compose (dรก para tirar o `guacd` e a rede se nรฃo pretendes usar o desktop remoto): ```yaml services: @@ -291,11 +350,37 @@ networks: driver: bridge ``` +### Linha de comando + +O Termix tambรฉm tem um CLI, para gerenciares os teus servidores pelo terminal e usares o Termix nos teus prรณprios scripts. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +Ele abre terminais, roda um comando num host ou numa frota inteira, move arquivos por SFTP e gerencia hosts, trechos e credenciais. A documentaรงรฃo completa estรก em [docs.termix.site/cli](https://docs.termix.site/cli). + +### Hospedagem na nuvem + +Dรก para rodar o servidor do Termix num VPS em vez de dentro da tua prรณpria rede. Se o Termix roda na rede que ele gerencia, uma queda leva ele junto, bem na hora em que precisas dele para resolver. Rodando fora ele continua acessรญvel, te dรก um IP fixo e dรก para entrar de qualquer lugar sem VPN nem abrir portas. + +A [GINERNET](https://docs.termix.site/install/ginernet) patrocina o Termix, e a documentaรงรฃo tem um guia passo a passo para publicar na plataforma de VPS deles. + +
+ +## Telemetria + +O Termix manda uma vez por dia um pequeno sinal anรดnimo, para eu saber quantas instรขncias estรฃo rodando e quais funcionalidades sรฃo usadas. Ele contรฉm um ID de instรขncia aleatรณrio, quantos usuรกrios e hosts tu tens, a versรฃo do aplicativo e quais funcionalidades (terminal, gerenciador de arquivos, tรบneis, docker, etc.) foram usadas nas รบltimas 24 horas. Nunca contรฉm nomes de usuรกrio, nomes de host, endereรงos IP, credenciais ou qualquer coisa que identifique ti ou os teus servidores. + +Vem ligado por padrรฃo. Podes desligar nas configuraรงรตes de administraรงรฃo, em Geral, ou definir `ENABLE_TELEMETRY=false` antes mesmo de iniciar o Termix. +
## Doar -Termix e gratuito e de codigo aberto, sem assinaturas ou planos pagos. Se o achar util, considere doar para ajudar a cobrir custos de servidor, dominios e tempo de desenvolvimento. As doacoes tambem ajudam a financiar o tempo de pesquisa e aprendizado necessario para construir funcionalidades como suporte a SAML, Kubernetes e Agent. Acompanhe o progresso e doe abaixo. +O Termix รฉ gratuito e de cรณdigo aberto, sem assinaturas nem planos pagos. Se ele te ajuda, considera doar para ajudar com servidores, domรญnios e tempo de desenvolvimento. As doaรงรตes tambรฉm custeiam o tempo de pesquisar e aprender o necessรกrio para funcionalidades como SAML, Kubernetes e suporte a agentes. Acompanha o progresso e doa abaixo. [Doar](https://donate.termix.site/) @@ -303,7 +388,7 @@ Termix e gratuito e de codigo aberto, sem assinaturas ou planos pagos. Se o acha ## Patrocinadores -Interessado em um espaco pago para apoiar o desenvolvimento? Envie um email para [mail@termix.site](mailto:mail@termix.site). +Tens interesse num espaรงo pago para apoiar o desenvolvimento? Escreve para [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Interessado em um espaco pago para apoiar o desenvolvimento? Envie um email para Cloudflare     - - Tailscale - -    Akamai @@ -340,18 +421,21 @@ Interessado em um espaco pago para apoiar o desenvolvimento? Envie um email para Rack Genius - +    + + Ginernet +

## Suporte -Se voce precisa de ajuda ou deseja solicitar uma funcionalidade para o Termix, visite a pagina de [Issues](https://github.com/Termix-SSH/Support/issues), faca login e clique em `New Issue`. Por favor, seja o mais detalhado possivel no seu relato, preferencialmente escrito em ingles. Voce tambem pode entrar no servidor do [Discord](https://discord.gg/jVQGdvHDrf) e visitar o canal de suporte, porem, os tempos de resposta podem ser mais longos. +Precisas de ajuda ou queres pedir uma funcionalidade? Abre uma [nova issue](https://github.com/Termix-SSH/Support/issues) com o mรกximo de detalhes possรญvel, em inglรชs se der. Tambรฉm podes perguntar no canal de suporte do [Discord](https://discord.gg/jVQGdvHDrf), embora as respostas por lรก possam demorar mais.
-## Capturas de Tela +## Capturas de tela
@@ -359,7 +443,7 @@ Se voce precisa de ajuda ou deseja solicitar uma funcionalidade para o Termix, v [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -Assista resumos de atualizacoes no YouTube +Vรช as apresentaรงรตes das atualizaรงรตes no YouTube

@@ -399,18 +483,18 @@ Se voce precisa de ajuda ou deseja solicitar uma funcionalidade para o Termix, v
PlataformaDistribuicaoDistribuiรงรฃo
WebQualquer navegador moderno (Chrome, Safari, Firefox) ยท Suporte PWAQualquer navegador moderno (Chrome, Safari, Firefox) ยท Suporte a PWA
Windows x64/ia32Portatil ยท Instalador MSI ยท ChocolateyPortรกtil ยท Instalador MSI ยท Chocolatey
Linux x64/ia32Portatil ยท AUR ยท AppImage ยท Deb ยท FlatpakPortรกtil ยท AUR ยท AppImage ยท Deb ยท Flatpak
macOS x64/ia32, v12.0+
-Alguns videos e imagens podem estar desatualizados ou podem nao mostrar perfeitamente as funcionalidades. +Alguns vรญdeos e imagens podem estar desatualizados ou nรฃo mostrar as funcionalidades perfeitamente.
-## Funcionalidades Planejadas +## Funcionalidades planejadas -Consulte [Projetos](https://github.com/orgs/Termix-SSH/projects/5) para todas as funcionalidades planejadas. Se voce deseja contribuir, consulte [Contribuir](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +Todas as funcionalidades planejadas estรฃo em [Projects](https://github.com/orgs/Termix-SSH/projects/5). Se queres contribuir, vรช [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
-## Licenca +## Licenรงa -Distribuido sob a Licenca Apache Versao 2.0. Consulte `LICENSE` para mais informacoes. +Distribuรญdo sob a Licenรงa Apache versรฃo 2.0. Vรช `LICENSE` para mais informaรงรตes. diff --git a/docs/readme/README-RU.md b/docs/readme/README-RU.md index 7db61e9..2c9115a 100644 --- a/docs/readme/README-RU.md +++ b/docs/readme/README-RU.md @@ -4,7 +4,7 @@

Termix

-

ะกะฐะผะพัั‚ะพัั‚ะตะปัŒะฝะพ ั€ะฐะทะผะตั‰ะฐะตะผะพะต ัƒะฟั€ะฐะฒะปะตะฝะธะต SSH ะธ ะดะพัั‚ัƒะฟ ะบ ัƒะดะฐะปั‘ะฝะฝะพะผัƒ ั€ะฐะฑะพั‡ะตะผัƒ ัั‚ะพะปัƒ

+

ะฃะฟั€ะฐะฒะปะตะฝะธะต ัะตั€ะฒะตั€ะฐะผะธ ะฝะฐ ัะฒะพั‘ะผ ั…ะพัั‚ะธะฝะณะต, ะพั‚ SSH ะธ ัƒะดะฐะปั‘ะฝะฝะพะณะพ ั€ะฐะฑะพั‡ะตะณะพ ัั‚ะพะปะฐ ะดะพ ะฐะฒั‚ะพะผะฐั‚ะธะทะฐั†ะธะน

English ยท @@ -37,7 +37,7 @@
-Termix โ€” ะฑะตัะฟะปะฐั‚ะฝั‹ะน ะฟั€ะพะตะบั‚ ั ะพั‚ะบั€ั‹ั‚ั‹ะผ ะธัั…ะพะดะฝั‹ะผ ะบะพะดะพะผ. ะ•ัะปะธ ะพะฝ ะฒะฐะผ ะฟะพะปะตะทะตะฝ, ั€ะฐััะผะพั‚ั€ะธั‚ะต ะฒะพะทะผะพะถะฝะพัั‚ัŒ [ะฟะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั](https://donate.termix.site/) ะดะปั ะฟะพะบั€ั‹ั‚ะธั ั€ะฐัั…ะพะดะพะฒ ะฝะฐ ัะตั€ะฒะตั€ั‹ ะธ ะฒั€ะตะผั ั€ะฐะทั€ะฐะฑะพั‚ะบะธ. +Termix ะฑะตัะฟะปะฐั‚ะตะฝ ะธ ั ะพั‚ะบั€ั‹ั‚ั‹ะผ ะธัั…ะพะดะฝั‹ะผ ะบะพะดะพะผ. ะ•ัะปะธ ะพะฝ ะฒะฐะผ ะฟั€ะธะณะพะดะธะปัั, ะฟะพะดัƒะผะฐะนั‚ะต ะพ [ะฟะพะถะตั€ั‚ะฒะพะฒะฐะฝะธะธ](https://donate.termix.site/), ั‡ั‚ะพะฑั‹ ะฟะพะผะพั‡ัŒ ะฟะพะบั€ั‹ั‚ัŒ ั€ะฐัั…ะพะดั‹ ะฝะฐ ัะตั€ะฒะตั€ั‹ ะธ ะฒั€ะตะผั ะฝะฐ ั€ะฐะทั€ะฐะฑะพั‚ะบัƒ.
@@ -49,7 +49,7 @@ Termix โ€” ะฑะตัะฟะปะฐั‚ะฝั‹ะน ะฟั€ะพะตะบั‚ ั ะพั‚ะบั€ั‹ั‚ั‹ะผ ะธัั…ะพะดะฝั‹

Repo of the Day Achievement
- ะ”ะพัั‚ะธะณะฝัƒั‚ะพ 1 ัะตะฝั‚ัะฑั€ั 2025 ะณะพะดะฐ + ะŸะพะปัƒั‡ะตะฝะพ 1 ัะตะฝั‚ัะฑั€ั 2025 ะณะพะดะฐ

@@ -58,7 +58,7 @@ Termix โ€” ะฑะตัะฟะปะฐั‚ะฝั‹ะน ะฟั€ะพะตะบั‚ ั ะพั‚ะบั€ั‹ั‚ั‹ะผ ะธัั…ะพะดะฝั‹ ## ะžะฑะทะพั€ -Termix - ัั‚ะพ ะฟะปะฐั‚ั„ะพั€ะผะฐ ะดะปั ัƒะฟั€ะฐะฒะปะตะฝะธั ัะตั€ะฒะตั€ะฐะผะธ ั ะพั‚ะบั€ั‹ั‚ั‹ะผ ะธัั…ะพะดะฝั‹ะผ ะบะพะดะพะผ, ะฝะฐะฒัะตะณะดะฐ ะฑะตัะฟะปะฐั‚ะฝะฐั ะธ ั€ะฐะทะผะตั‰ะฐะตะผะฐั ะฝะฐ ัะพะฑัั‚ะฒะตะฝะฝะพะผ ัะตั€ะฒะตั€ะต. ะžะฝะฐ ะฟั€ะตะดะพัั‚ะฐะฒะปัะตั‚ ะผัƒะปัŒั‚ะธะฟะปะฐั‚ั„ะพั€ะผะตะฝะฝะพะต ั€ะตัˆะตะฝะธะต ะดะปั ัƒะฟั€ะฐะฒะปะตะฝะธั ะฒะฐัˆะธะผะธ ัะตั€ะฒะตั€ะฐะผะธ ะธ ะธะฝั„ั€ะฐัั‚ั€ัƒะบั‚ัƒั€ะพะน ั‡ะตั€ะตะท ะตะดะธะฝั‹ะน ะธะฝั‚ัƒะธั‚ะธะฒะฝะพ ะฟะพะฝัั‚ะฝั‹ะน ะธะฝั‚ะตั€ั„ะตะนั. Termix ะฟั€ะตะดะปะฐะณะฐะตั‚ ะดะพัั‚ัƒะฟ ะบ SSH-ั‚ะตั€ะผะธะฝะฐะปัƒ, ัƒะฟั€ะฐะฒะปะตะฝะธะต ัƒะดะฐะปะตะฝะฝั‹ะผ ั€ะฐะฑะพั‡ะธะผ ัั‚ะพะปะพะผ (RDP, VNC, Telnet), ะฒะพะทะผะพะถะฝะพัั‚ะธ SSH-ั‚ัƒะฝะฝะตะปะธั€ะพะฒะฐะฝะธั, ัƒะดะฐะปะตะฝะฝะพะต ัƒะฟั€ะฐะฒะปะตะฝะธะต ั„ะฐะนะปะฐะผะธ SSH ะธ ะผะฝะพะถะตัั‚ะฒะพ ะดั€ัƒะณะธั… ะธะฝัั‚ั€ัƒะผะตะฝั‚ะพะฒ. Termix - ัั‚ะพ ะธะดะตะฐะปัŒะฝะฐั ะฑะตัะฟะปะฐั‚ะฝะฐั ะฐะปัŒั‚ะตั€ะฝะฐั‚ะธะฒะฐ Termius ั ะฒะพะทะผะพะถะฝะพัั‚ัŒัŽ ั€ะฐะทะผะตั‰ะตะฝะธั ะฝะฐ ัะพะฑัั‚ะฒะตะฝะฝะพะผ ัะตั€ะฒะตั€ะต, ะดะพัั‚ัƒะฟะฝะฐั ะดะปั ะฒัะตั… ะฟะปะฐั‚ั„ะพั€ะผ. +Termix ัั‚ะพ ะฑะตัะฟะปะฐั‚ะฝะฐั ะฟะปะฐั‚ั„ะพั€ะผะฐ ั ะพั‚ะบั€ั‹ั‚ั‹ะผ ะธัั…ะพะดะฝั‹ะผ ะบะพะดะพะผ ะดะปั ัƒะฟั€ะฐะฒะปะตะฝะธั ัะตั€ะฒะตั€ะฐะผะธ ะฝะฐ ัะฒะพั‘ะผ ั…ะพัั‚ะธะฝะณะต. ะžะฝะฐ ัะพะฑะธั€ะฐะตั‚ ะฒ ะพะดะฝะพะผ ะผะตัั‚ะต SSH-ั‚ะตั€ะผะธะฝะฐะปั‹, ัƒะดะฐะปั‘ะฝะฝั‹ะต ั€ะฐะฑะพั‡ะธะต ัั‚ะพะปั‹ (RDP, VNC, Telnet), ะฟะตั€ะตะดะฐั‡ัƒ ั„ะฐะนะปะพะฒ, ั‚ัƒะฝะฝะตะปะธ, Docker, ะผะตั‚ั€ะธะบะธ ะธ ะฐะฒั‚ะพะผะฐั‚ะธะทะฐั†ะธะธ, ะฒ ะฑั€ะฐัƒะทะตั€ะต, ะฝะฐ ะบะพะผะฟัŒัŽั‚ะตั€ะต ะธ ะฝะฐ ั‚ะตะปะตั„ะพะฝะต. ะญั‚ะพ self-hosted ะทะฐะผะตะฝะฐ Termius, ะบะพั‚ะพั€ะฐั ะพัั‚ะฐั‘ั‚ัั ะฑะตัะฟะปะฐั‚ะฝะพะน ะฝะฐะฒัะตะณะดะฐ.
@@ -68,126 +68,182 @@ Termix - ัั‚ะพ ะฟะปะฐั‚ั„ะพั€ะผะฐ ะดะปั ัƒะฟั€ะฐะฒะปะตะฝะธั ัะตั€ะฒะตั€ะฐะผ -**ะ”ะพัั‚ัƒะฟ ะบ SSH-ั‚ะตั€ะผะธะฝะฐะปัƒ:** -ะŸะพะปะฝะพั„ัƒะฝะบั†ะธะพะฝะฐะปัŒะฝั‹ะน ั‚ะตั€ะผะธะฝะฐะป ั ะฟะพะดะดะตั€ะถะบะพะน ั€ะฐะทะดะตะปะตะฝะธั ัะบั€ะฐะฝะฐ (ะดะพ 4 ะฟะฐะฝะตะปะตะน) ะธ ัะธัั‚ะตะผะพะน ะฒะบะปะฐะดะพะบ, ะบะฐะบ ะฒ ะฑั€ะฐัƒะทะตั€ะต. ะ’ะบะปัŽั‡ะฐะตั‚ ะฟะพะดะดะตั€ะถะบัƒ ะฝะฐัั‚ั€ะพะนะบะธ ั‚ะตั€ะผะธะฝะฐะปะฐ, ะฒะบะปัŽั‡ะฐั ะฟะพะฟัƒะปัั€ะฝั‹ะต ั‚ะตะผั‹, ัˆั€ะธั„ั‚ั‹ ะธ ะดั€ัƒะณะธะต ะบะพะผะฟะพะฝะตะฝั‚ั‹. +**SSH-ั‚ะตั€ะผะธะฝะฐะป:** +ะŸะพะปะฝะพั†ะตะฝะฝั‹ะน ั‚ะตั€ะผะธะฝะฐะป ั ะฒะบะปะฐะดะบะฐะผะธ ะบะฐะบ ะฒ ะฑั€ะฐัƒะทะตั€ะต ะธ ั€ะฐะทะดะตะปะตะฝะธะตะผ ัะบั€ะฐะฝะฐ, ะดะพ 6 ะฟะฐะฝะตะปะตะน ะพะดะฝะพะฒั€ะตะผะตะฝะฝะพ. ะขะตะผัƒ, ัˆั€ะธั„ั‚ ะธ ั†ะฒะตั‚ะฐ ะฒั‹ะฑะธั€ะฐะตั‚ะต ะฒั‹. ะะฐะด ะบะฐะถะดะพะน ัะตััะธะตะน ะตัั‚ัŒ ะฟะฐะฝะตะปัŒ ั ั‚ะตะบัƒั‰ะตะน ะทะฐะณั€ัƒะทะบะพะน ะฟั€ะพั†ะตััะพั€ะฐ, ะฟะฐะผัั‚ะธ ะธ ะดะธัะบะฐ, ะฐ ั‚ะฐะบะถะต ะฑั‹ัั‚ั€ั‹ะต ััั‹ะปะบะธ ะฝะฐ ั„ะฐะนะปั‹, Docker, ั‚ัƒะฝะฝะตะปะธ ะธ ะผะตั‚ั€ะธะบะธ ัั‚ะพะณะพ ั…ะพัั‚ะฐ. -**ะ”ะพัั‚ัƒะฟ ะบ ัƒะดะฐะปั‘ะฝะฝะพะผัƒ ั€ะฐะฑะพั‡ะตะผัƒ ัั‚ะพะปัƒ:** -ะŸะพะดะดะตั€ะถะบะฐ RDP, VNC ะธ Telnet ั‡ะตั€ะตะท ะฑั€ะฐัƒะทะตั€ ั ะฟะพะปะฝะพะน ะฝะฐัั‚ั€ะพะนะบะพะน ะธ ั€ะฐะทะดะตะปะตะฝะธะตะผ ัะบั€ะฐะฝะฐ. +**ะฃะดะฐะปั‘ะฝะฝั‹ะน ั€ะฐะฑะพั‡ะธะน ัั‚ะพะป:** +RDP, VNC ะธ Telnet ะฟั€ัะผะพ ะฒ ะฑั€ะฐัƒะทะตั€ะต, ะฒะพ ะฒะบะปะฐะดะบะฐั… ะธ ั ั€ะฐะทะดะตะปะตะฝะธะตะผ ัะบั€ะฐะฝะฐ, ะบะฐะบ ะธ ะปัŽะฑะฐั ะดั€ัƒะณะฐั ัะตััะธั. ะ•ัั‚ัŒ ะฟั€ะพัะผะพั‚ั€ ั„ะฐะนะปะพะฒ ะฝะฐ ะดะธัะบะฐั… RDP ะธ ะทะฐะณั€ัƒะทะบะฐ ะฟะตั€ะตั‚ะฐัะบะธะฒะฐะฝะธะตะผ. ะ’ ะฒะตั€ัะธะธ ะดะปั Windows ั…ะพัั‚ ะผะพะถะฝะพ ะพั‚ะบั€ั‹ั‚ัŒ ะธ ะฒ ะพะฑั‹ั‡ะฝะพะผ ะบะปะธะตะฝั‚ะต RDP. -**ะฃะฟั€ะฐะฒะปะตะฝะธะต SSH-ั‚ัƒะฝะฝะตะปัะผะธ:** -ะกะพะทะดะฐะฝะธะต ะธ ัƒะฟั€ะฐะฒะปะตะฝะธะต ะผะตะถัะตั€ะฒะตั€ะฝั‹ะผะธ SSH-ั‚ัƒะฝะฝะตะปัะผะธ ั ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธะผ ะฟะตั€ะตะฟะพะดะบะปัŽั‡ะตะฝะธะตะผ, ะผะพะฝะธั‚ะพั€ะธะฝะณะพะผ ัะพัั‚ะพัะฝะธั ะธ ะปะพะบะฐะปัŒะฝะพะน, ัƒะดะฐะปั‘ะฝะฝะพะน ะธะปะธ ะดะธะฝะฐะผะธั‡ะตัะบะพะน SOCKS-ะฟะตั€ะตะฐะดั€ะตัะฐั†ะธะตะน. ะะฐัั‚ั€ะพะนะบะธ ั‚ัƒะฝะฝะตะปะตะน ยซะดะตัะบั‚ะพะฟะฝั‹ะน ะบะปะธะตะฝั‚ - ัะตั€ะฒะตั€ยป ั…ั€ะฐะฝัั‚ัั ะปะพะบะฐะปัŒะฝะพ ะดะปั ะบะฐะถะดะพะน ัƒัั‚ะฐะฝะพะฒะบะธ; ะพะฟั†ะธะพะฝะฐะปัŒะฝั‹ะต ัะฝะธะผะบะธ C2S-ะฟั€ะตัะตั‚ะพะฒ ะผะพะถะฝะพ ัะพั…ั€ะฐะฝัั‚ัŒ ะฝะฐ ัะตั€ะฒะตั€ะต, ะฟะตั€ะตะธะผะตะฝะพะฒั‹ะฒะฐั‚ัŒ, ะทะฐะณั€ัƒะถะฐั‚ัŒ ะธะปะธ ัƒะดะฐะปัั‚ัŒ, ะบะพะณะดะฐ ะฒั‹ ั…ะพั‚ะธั‚ะต ะฟะตั€ะตะฝะตัั‚ะธ ะปะพะบะฐะปัŒะฝัƒัŽ ะบะพะฝั„ะธะณัƒั€ะฐั†ะธัŽ ั‚ัƒะฝะฝะตะปั ะผะตะถะดัƒ ะบะปะธะตะฝั‚ะฐะผะธ. +**SSH-ั‚ัƒะฝะฝะตะปะธ:** +ะ›ะพะบะฐะปัŒะฝะฐั, ัƒะดะฐะปั‘ะฝะฝะฐั ะธ ะดะธะฝะฐะผะธั‡ะตัะบะฐั ะฟะตั€ะตะฐะดั€ะตัะฐั†ะธั SOCKS ั ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธะผ ะฟะตั€ะตะฟะพะดะบะปัŽั‡ะตะฝะธะตะผ ะธ ะฟั€ะพะฒะตั€ะบะพะน ัะพัั‚ะพัะฝะธั. ะขัƒะฝะฝะตะปะธ ะพั‚ ะบะปะธะตะฝั‚ะฐ ะบ ัะตั€ะฒะตั€ัƒ ะฒ ะฝะฐัั‚ะพะปัŒะฝะพะผ ะฟั€ะธะปะพะถะตะฝะธะธ ั…ั€ะฐะฝัั‚ัั ะฝะฐ ัั‚ะพะผ ะบะพะผะฟัŒัŽั‚ะตั€ะต, ะฐ ะฝะฐะฑะพั€ั‹ ะฝะฐัั‚ั€ะพะตะบ ะผะพะถะฝะพ ัะพั…ั€ะฐะฝะธั‚ัŒ ะฝะฐ ัะตั€ะฒะตั€ะต, ั‡ั‚ะพะฑั‹ ะฟะตั€ะตะฝะตัั‚ะธ ะบะพะฝั„ะธะณัƒั€ะฐั†ะธัŽ ะฝะฐ ะดั€ัƒะณัƒัŽ ะผะฐัˆะธะฝัƒ. -**ะฃะดะฐะปั‘ะฝะฝั‹ะน ั„ะฐะนะปะพะฒั‹ะน ะผะตะฝะตะดะถะตั€:** -ะฃะฟั€ะฐะฒะปะตะฝะธะต ั„ะฐะนะปะฐะผะธ ะฝะตะฟะพัั€ะตะดัั‚ะฒะตะฝะฝะพ ะฝะฐ ัƒะดะฐะปั‘ะฝะฝั‹ั… ัะตั€ะฒะตั€ะฐั… ั ะฟะพะดะดะตั€ะถะบะพะน ะฟั€ะพัะผะพั‚ั€ะฐ ะธ ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธั ะบะพะดะฐ, ะธะทะพะฑั€ะฐะถะตะฝะธะน, ะฐัƒะดะธะพ ะธ ะฒะธะดะตะพ. ะ—ะฐะณั€ัƒะทะบะฐ, ัะบะฐั‡ะธะฒะฐะฝะธะต, ะฟะตั€ะตะธะผะตะฝะพะฒะฐะฝะธะต, ัƒะดะฐะปะตะฝะธะต ะธ ะฟะตั€ะตะผะตั‰ะตะฝะธะต ั„ะฐะนะปะพะฒ ั ะฟะพะดะดะตั€ะถะบะพะน sudo. ะ’ะบะปัŽั‡ะฐะตั‚ ะฟะพะดะดะตั€ะถะบัƒ ะฟะตั€ะตะผะตั‰ะตะฝะธั ั„ะฐะนะปะพะฒ ั ัะตั€ะฒะตั€ะฐ ะฝะฐ ัะตั€ะฒะตั€. +**ะคะฐะนะปะพะฒั‹ะน ะผะตะฝะตะดะถะตั€:** +ะŸั€ะพัะผะฐั‚ั€ะธะฒะฐะนั‚ะต, ั€ะตะดะฐะบั‚ะธั€ัƒะนั‚ะต, ะทะฐะณั€ัƒะถะฐะนั‚ะต, ัะบะฐั‡ะธะฒะฐะนั‚ะต, ะฟะตั€ะตะธะผะตะฝะพะฒั‹ะฒะฐะนั‚ะต, ะฟะตั€ะตะผะตั‰ะฐะนั‚ะต ะธ ัƒะดะฐะปัะนั‚ะต ั„ะฐะนะปั‹ ะฟะพ SFTP, ะฒ ั‚ะพะผ ั‡ะธัะปะต ั‡ะตั€ะตะท sudo. ะกะผะพั‚ั€ะธั‚ะต ะธ ะฟั€ะฐะฒัŒั‚ะต ะบะพะด, ะธะทะพะฑั€ะฐะถะตะฝะธั, ะฐัƒะดะธะพ ะธ ะฒะธะดะตะพ. ะšะพะฟะธั€ัƒะนั‚ะต ั„ะฐะนะปั‹ ะฝะฐะฟั€ัะผัƒัŽ ั ะพะดะฝะพะณะพ ัะตั€ะฒะตั€ะฐ ะฝะฐ ะดั€ัƒะณะพะน: ัะฐะผั‹ะน ะฑั‹ัั‚ั€ั‹ะน ะผะฐั€ัˆั€ัƒั‚ ะฟะพะดะฑะธั€ะฐะตั‚ัั ัะฐะผ, ะฐ ั†ะตะปะพัั‚ะฝะพัั‚ัŒ ะฟะตั€ะตะดะฐั‡ะธ ะฟั€ะพะฒะตั€ัะตั‚ัั. -**ะฃะฟั€ะฐะฒะปะตะฝะธะต Docker ะธ Podman:** -ะ—ะฐะฟัƒัะบ, ะพัั‚ะฐะฝะพะฒะบะฐ, ะฟั€ะธะพัั‚ะฐะฝะพะฒะบะฐ, ัƒะดะฐะปะตะฝะธะต ะบะพะฝั‚ะตะนะฝะตั€ะพะฒ. ะŸั€ะพัะผะพั‚ั€ ัั‚ะฐั‚ะธัั‚ะธะบะธ ะบะพะฝั‚ะตะนะฝะตั€ะพะฒ. ะฃะฟั€ะฐะฒะปะตะฝะธะต ะบะพะฝั‚ะตะนะฝะตั€ะพะผ ั‡ะตั€ะตะท ั‚ะตั€ะผะธะฝะฐะป docker exec. ะŸะพะดะดะตั€ะถะธะฒะฐะตั‚ ะบะฐะบ Docker, ั‚ะฐะบ ะธ Podman ะฒ ะบะฐั‡ะตัั‚ะฒะต ัั€ะตะดั‹ ะฒั‹ะฟะพะปะฝะตะฝะธั ะบะพะฝั‚ะตะนะฝะตั€ะพะฒ. ะะต ะฟั€ะตะดะฝะฐะทะฝะฐั‡ะตะฝ ะดะปั ะทะฐะผะตะฝั‹ Portainer ะธะปะธ Dockge, ะฐ ัะบะพั€ะตะต ะดะปั ะฟั€ะพัั‚ะพะณะพ ัƒะฟั€ะฐะฒะปะตะฝะธั ะบะพะฝั‚ะตะนะฝะตั€ะฐะผะธ ะฟะพ ัั€ะฐะฒะฝะตะฝะธัŽ ั ะธั… ัะพะทะดะฐะฝะธะตะผ. +**Docker ะธ Podman:** +ะ—ะฐะฟัƒัะบะฐะนั‚ะต, ะพัั‚ะฐะฝะฐะฒะปะธะฒะฐะนั‚ะต, ัั‚ะฐะฒัŒั‚ะต ะฝะฐ ะฟะฐัƒะทัƒ ะธ ัƒะดะฐะปัะนั‚ะต ะบะพะฝั‚ะตะนะฝะตั€ั‹, ัะผะพั‚ั€ะธั‚ะต ะธั… ะฝะฐะณั€ัƒะทะบัƒ ะธ ะพั‚ะบั€ั‹ะฒะฐะนั‚ะต ะพะฑะพะปะพั‡ะบัƒ ะฒะฝัƒั‚ั€ะธ. ะ ะฐะฑะพั‚ะฐะตั‚ ะธ ั Docker, ะธ ั Podman. ะญั‚ะพ ะฝะต ะทะฐะผะตะฝะฐ Portainer ะธะปะธ Dockge, ะฐ ัะฟะพัะพะฑ ัƒะฟั€ะฐะฒะปัั‚ัŒ ั‚ะตะผะธ ะบะพะฝั‚ะตะนะฝะตั€ะฐะผะธ, ั‡ั‚ะพ ัƒ ะฒะฐั ัƒะถะต ะตัั‚ัŒ. -**ะœะตะฝะตะดะถะตั€ SSH-ั…ะพัั‚ะพะฒ:** -ะกะพั…ั€ะฐะฝะตะฝะธะต, ะพั€ะณะฐะฝะธะทะฐั†ะธั ะธ ัƒะฟั€ะฐะฒะปะตะฝะธะต SSH-ะฟะพะดะบะปัŽั‡ะตะฝะธัะผะธ ั ะฟะพะผะพั‰ัŒัŽ ั‚ะตะณะพะฒ ะธ ะฟะฐะฟะพะบ (ั ะฝะฐัั‚ั€ะพะนะบะพะน ะฟะฐะฟะพะบ ะธ ะฟะพะดะดะตั€ะถะบะพะน ะฒะปะพะถะตะฝะฝั‹ั… ะฟะฐะฟะพะบ), ั ะฒะพะทะผะพะถะฝะพัั‚ัŒัŽ ัะพั…ั€ะฐะฝะตะฝะธั ะดะฐะฝะฝั‹ั… ะดะปั ะฟะพะฒั‚ะพั€ะฝะพะณะพ ะฒั…ะพะดะฐ ะธ ะฐะฒั‚ะพะผะฐั‚ะธะทะฐั†ะธะธ ั€ะฐะทะฒั‘ั€ั‚ั‹ะฒะฐะฝะธั SSH-ะบะปัŽั‡ะตะน. +**ะœะตะฝะตะดะถะตั€ ั…ะพัั‚ะพะฒ:** +ะฅั€ะฐะฝะธั‚ะต ะธ ัƒะฟะพั€ัะดะพั‡ะธะฒะฐะนั‚ะต ั…ะพัั‚ั‹ ั ะฟะพะผะพั‰ัŒัŽ ะผะตั‚ะพะบ ะธ ะฒะปะพะถะตะฝะฝั‹ั… ะฟะฐะฟะพะบ, ะบะพั‚ะพั€ั‹ะผ ะผะพะถะฝะพ ะทะฐะดะฐั‚ัŒ ะธะผั ะธ ั†ะฒะตั‚. ะ˜ัะฟะพะปัŒะทัƒะนั‚ะต ัะพั…ั€ะฐะฝั‘ะฝะฝั‹ะต ัƒั‡ั‘ั‚ะฝั‹ะต ะดะฐะฝะฝั‹ะต ะฝะฐ ะฝะตัะบะพะปัŒะบะธั… ั…ะพัั‚ะฐั…, ั€ะฐะทะฒะพั€ะฐั‡ะธะฒะฐะนั‚ะต SSH-ะบะปัŽั‡ะธ ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ, ะณั€ัƒะฟะฟะธั€ัƒะนั‚ะต ั…ะพัั‚ั‹ ะฟะพะด ั€ะพะดะธั‚ะตะปัŒัะบะธะผ, ั€ะตะดะฐะบั‚ะธั€ัƒะนั‚ะต ะธ ะฒั‹ะณั€ัƒะถะฐะนั‚ะต ะฟะฐะบะตั‚ะฝะพ, ะฐ ะดะปั ั€ะฐะทะพะฒั‹ั… ะฟะพะดะบะปัŽั‡ะตะฝะธะน, ะบะพั‚ะพั€ั‹ะต ะฝะต ั…ะพั‡ะตั‚ัั ัะพั…ั€ะฐะฝัั‚ัŒ, ะตัั‚ัŒ ะฑั‹ัั‚ั€ะพะต ะฟะพะดะบะปัŽั‡ะตะฝะธะต. -**ะœะตั‚ั€ะธะบะธ ั…ะพัั‚ะฐ:** -ะŸั€ะพัะผะพั‚ั€ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธั CPU, ะฟะฐะผัั‚ะธ ะธ ะดะธัะบะฐ, ัะตั‚ะธ, ะฒั€ะตะผะตะฝะธ ั€ะฐะฑะพั‚ั‹, ะธะฝั„ะพั€ะผะฐั†ะธะธ ะพ ัะธัั‚ะตะผะต, ั„ะฐะนั€ะฒะพะปะฐ, ะผะพะฝะธั‚ะพั€ะฐ ะฟะพั€ั‚ะพะฒ, ะฟั€ะพัะผะพั‚ั€ั‰ะธะบะฐ ะปะพะณะพะฒ, ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน/ะฟั€ะฐะฒ ะดะพัั‚ัƒะฟะฐ, ัะตั€ั‚ะธั„ะธะบะฐั‚ะพะฒ ะธ ะผะฝะพะณะพะณะพ ะดั€ัƒะณะพะณะพ ะฝะฐ ะฑะพะปัŒัˆะธะฝัั‚ะฒะต ัะตั€ะฒะตั€ะพะฒ ะฝะฐ ะฑะฐะทะต Linux. ะ’ะบะปัŽั‡ะฐะตั‚ ะณั€ะฐั„ะธะบะธ ะธัั‚ะพั€ะธะธ ะฒั€ะตะผะตะฝะฝั‹ั… ั€ัะดะพะฒ ะธ ะพะฟะพะฒะตั‰ะตะฝะธั ะฝะฐ ะพัะฝะพะฒะต ะฟะพั€ะพะณะพะฒั‹ั… ะทะฝะฐั‡ะตะฝะธะน ั ะฟะพะดะดะตั€ะถะบะพะน ntfy ะธ ะฒะตะฑั…ัƒะบะพะฒ. +**ะœะตั‚ั€ะธะบะธ ั…ะพัั‚ะพะฒ:** +ะŸั€ะพั†ะตััะพั€, ะฟะฐะผัั‚ัŒ, ะดะธัะบ, ัะตั‚ัŒ, ั‚ะตะผะฟะตั€ะฐั‚ัƒั€ะฐ, ะฒั€ะตะผั ั€ะฐะฑะพั‚ั‹, ะฟั€ะพั†ะตััั‹, ะฟะพั€ั‚ั‹, ะฒั…ะพะดั‹ ะฒ ัะธัั‚ะตะผัƒ ะธ ัะฒะตะดะตะฝะธั ะพ ัะธัั‚ะตะผะต ะฝะฐ ะฑะพะปัŒัˆะธะฝัั‚ะฒะต ัะตั€ะฒะตั€ะพะฒ Linux, ั ะณั€ะฐั„ะธะบะฐะผะธ ะทะฐ ะฟั€ะพัˆะปั‹ะต ะฟะตั€ะธะพะดั‹. ะšะฐั€ั‚ะพั‡ะบะธ ัƒะฟั€ะฐะฒะปะตะฝะธั ะฟะพะทะฒะพะปััŽั‚ ั€ะฐะฑะพั‚ะฐั‚ัŒ ัะพ ัะปัƒะถะฑะฐะผะธ, ะทะฐะดะฐั‡ะฐะผะธ cron, ะฟะฐะบะตั‚ะฐะผะธ, ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัะผะธ, ะฟั€ะฐะฒะธะปะฐะผะธ ะฑั€ะฐะฝะดะผะฐัƒัั€ะฐ, WireGuard, Tailscale, ัะตั€ั‚ะธั„ะธะบะฐั‚ะฐะผะธ SSL, ะถัƒั€ะฝะฐะปะฐะผะธ ะธ ะฟั€ะพะฒะตั€ะบะฐะผะธ ัะพัั‚ะพัะฝะธั, ะฝะต ะฒั‹ั…ะพะดั ะธะท Termix. -**ะัƒั‚ะตะฝั‚ะธั„ะธะบะฐั†ะธั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน:** -ะ‘ะตะทะพะฟะฐัะฝะพะต ัƒะฟั€ะฐะฒะปะตะฝะธะต ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัะผะธ ั ะฐะดะผะธะฝะธัั‚ั€ะฐั‚ะธะฒะฝั‹ะผ ะบะพะฝั‚ั€ะพะปะตะผ (ะผะพะถะตั‚ ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะธะฝั„ะพั€ะผะฐั†ะธัŽ ะดั€ัƒะณะธั… ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน) ะธ ะฟะพะดะดะตั€ะถะบะพะน OIDC/LDAP/SSO (ั ะบะพะฝั‚ั€ะพะปะตะผ ะดะพัั‚ัƒะฟะฐ), 2FA (TOTP) ะธ ะฟะพะดะดะตั€ะถะบะพะน ะบะปัŽั‡ะตะน ะดะพัั‚ัƒะฟะฐ (WebAuthn). ะŸั€ะพัะผะพั‚ั€ ะฐะบั‚ะธะฒะฝั‹ั… ัะตััะธะน ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน ะฝะฐ ะฒัะตั… ะฟะปะฐั‚ั„ะพั€ะผะฐั… ะธ ะพั‚ะทั‹ะฒ ะฟั€ะฐะฒ ะดะพัั‚ัƒะฟะฐ. ะกะฒัะทั‹ะฒะฐะฝะธะต ะฐะบะบะฐัƒะฝั‚ะพะฒ OIDC/ะปะพะบะฐะปัŒะฝั‹ั… ะฐะบะบะฐัƒะฝั‚ะพะฒ. ะŸั€ะพัะผะพั‚ั€ ะถัƒั€ะฝะฐะปะฐ ะฐัƒะดะธั‚ะฐ ะดะตะนัั‚ะฒะธะน ะฒัะตั… ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน. +**ะะฒั‚ะพะผะฐั‚ะธะทะฐั†ะธะธ:** +ะ’ั‹ะฑะตั€ะธั‚ะต ัะพะฑั‹ั‚ะธะต, ะฐ ะทะฐั‚ะตะผ ะพะฟะธัˆะธั‚ะต, ั‡ั‚ะพ ะดะพะปะถะฝะพ ะฟั€ะพะธะทะพะนั‚ะธ. ะกะพะฑั‹ั‚ะธะตะผ ะผะพะถะตั‚ ะฑั‹ั‚ัŒ ะฟั€ะตะฒั‹ัˆะตะฝะธะต ะฟะพั€ะพะณะฐ ะผะตั‚ั€ะธะบะพะน, ั…ะพัั‚, ะบะพั‚ะพั€ั‹ะน ัƒะฟะฐะป ะธะปะธ ะฒะตั€ะฝัƒะปัั, ะธะทะผะตะฝะตะฝะธะต ะฟั€ะพะฒะตั€ะบะธ ัะพัั‚ะพัะฝะธั, ั€ะฐัะฟะธัะฐะฝะธะต, ัะพะฑั‹ั‚ะธะต ะบะพะฝั‚ะตะนะฝะตั€ะฐ ะธะปะธ ะฒั…ะพะดัั‰ะธะน webhook. ะจะฐะณะธ ัƒะผะตัŽั‚ ะฒั‹ะฟะพะปะฝัั‚ัŒ ะบะพะผะฐะฝะดั‹ ะธ ัะฝะธะฟะฟะตั‚ั‹, ัƒะฟั€ะฐะฒะปัั‚ัŒ ะบะพะฝั‚ะตะนะฝะตั€ะฐะผะธ ะธ ั‚ัƒะฝะฝะตะปัะผะธ, ะฑัƒะดะธั‚ัŒ ั…ะพัั‚, ะพะฑั€ะฐั‰ะฐั‚ัŒัั ะฟะพ ะฐะดั€ะตััƒ, ะถะดะฐั‚ัŒ, ะฒะตั‚ะฒะธั‚ัŒัั ะฟะพ ัƒัะปะพะฒะธัŽ, ะทะฐะฟัƒัะบะฐั‚ัŒ ะดั€ัƒะณัƒัŽ ะฐะฒั‚ะพะผะฐั‚ะธะทะฐั†ะธัŽ ะธ ะฟั€ะธัั‹ะปะฐั‚ัŒ ัƒะฒะตะดะพะผะปะตะฝะธั ั‡ะตั€ะตะท ntfy, Discord ะธะปะธ webhook. ะขะตัั‚ะพะฒั‹ะน ะทะฐะฟัƒัะบ ะฟะพะทะฒะพะปัะตั‚ ะฒัั‘ ะฑะตะทะพะฟะฐัะฝะพ ะฟั€ะพะฒะตั€ะธั‚ัŒ. -**ะ˜ะฝั‚ะตะณั€ะฐั†ะธั ั Tailscale:** -ะกะฟะธัะพะบ ัƒัั‚ั€ะพะนัั‚ะฒ ะฒะฐัˆะตะน ัะตั‚ะธ Tailscale ะดะปั ะฑั‹ัั‚ั€ะพะณะพ ะดะพะฑะฐะฒะปะตะฝะธั ะธั… ะฒ ะบะฐั‡ะตัั‚ะฒะต ั…ะพัั‚ะพะฒ ะธ ะฟะพะดะบะปัŽั‡ะตะฝะธะต ั‡ะตั€ะตะท Tailscale SSH ะฒ ะบะฐั‡ะตัั‚ะฒะต ะผะตั‚ะพะดะฐ ะฐัƒั‚ะตะฝั‚ะธั„ะธะบะฐั†ะธะธ, ะฟะพะทะฒะพะปัั ACL ะฒะฐัˆะตะน ัะตั‚ะธ ัƒะฟั€ะฐะฒะปัั‚ัŒ ะฐะฒั‚ะพั€ะธะทะฐั†ะธะตะน ะฑะตะท ั…ั€ะฐะฝะตะฝะธั ัƒั‡ั‘ั‚ะฝั‹ั… ะดะฐะฝะฝั‹ั…. +**ะคะปะพั‚ั‹:** +ะžะฑัŠะตะดะธะฝัะนั‚ะต ั…ะพัั‚ั‹ ะฒะพ ั„ะปะพั‚ ะฒั€ัƒั‡ะฝัƒัŽ ะธะปะธ ะฟะพ ะฟั€ะฐะฒะธะปะฐะผ ะผะตั‚ะพะบ, ั‡ั‚ะพะฑั‹ ะฝะพะฒั‹ะต ั…ะพัั‚ั‹ ะฟะพะฟะฐะดะฐะปะธ ั‚ัƒะดะฐ ัะฐะผะธ. ะ’ั‹ะฟะพะปะฝัะนั‚ะต ะพะดะฝัƒ ะบะพะผะฐะฝะดัƒ ัั€ะฐะทัƒ ะฝะฐ ะฒัะตั… ั…ะพัั‚ะฐั…, ะพั‚ะฟั€ะฐะฒะปัะนั‚ะต ะธ ะทะฐะฑะธั€ะฐะนั‚ะต ั„ะฐะนะปั‹ ัะพ ะฒัะตั…, ัƒัั‚ะฐะฝะฐะฒะปะธะฒะฐะนั‚ะต ะฟะฐะบะตั‚ั‹ ะธ ัะพะฑะธั€ะฐะนั‚ะต ัะฒะพะดะบัƒ ะฟะพ ัะธัั‚ะตะผะต, ัะดั€ัƒ, ะฐั€ั…ะธั‚ะตะบั‚ัƒั€ะต ะธ ะฒั€ะตะผะตะฝะธ ั€ะฐะฑะพั‚ั‹. -**RBAC/ะžะฑั‰ะธะน ะดะพัั‚ัƒะฟ:** -ะกะพะทะดะฐะฝะธะต ั€ะพะปะตะน ะธ ะฟั€ะตะดะพัั‚ะฐะฒะปะตะฝะธะต ะพะฑั‰ะตะณะพ ะดะพัั‚ัƒะฟะฐ ะบ ั…ะพัั‚ะฐะผ ะดะปั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน/ั€ะพะปะตะน. ะŸะพะดะดะตั€ะถะธะฒะฐะตั‚ ะฒัะต ั‚ะธะฟั‹ ะฐัƒั‚ะตะฝั‚ะธั„ะธะบะฐั†ะธะธ ะธ ะฒัะต ะฟั€ะพั‚ะพะบะพะปั‹ ั…ะพัั‚ะพะฒ. +**ะ˜ะ˜-ะฟะพะผะพั‰ะฝะธะบ:** +ะะตะพะฑัะทะฐั‚ะตะปัŒะฝะฐั ะฒะพะทะผะพะถะฝะพัั‚ัŒ, ะฒั‹ะบะปัŽั‡ะตะฝะฝะฐั ะดะพ ั‚ะตั… ะฟะพั€, ะฟะพะบะฐ ะฒั‹ ัะฐะผะธ ะตั‘ ะฝะต ะฒะบะปัŽั‡ะธั‚ะต. ะŸะพะดะบะปัŽั‡ะธั‚ะต OpenAI, Anthropic, Gemini, Ollama ะธะปะธ ะปัŽะฑะพะน ัะพะฒะผะตัั‚ะธะผั‹ะน ั OpenAI ะฐะดั€ะตั ะธ ัะฟั€ะฐัˆะธะฒะฐะนั‚ะต ะพ ัะฒะพะตะน ัะธัั‚ะตะผะต. ะžะฝ ั‡ะธั‚ะฐะตั‚ ั…ะพัั‚ั‹, ั„ะปะพั‚ั‹, ัะฝะธะฟะฟะตั‚ั‹ ะธ ะพะฟะพะฒะตั‰ะตะฝะธั ะธ ะฟั€ะตะดะปะฐะณะฐะตั‚ ะธะทะผะตะฝะตะฝะธั ะฝะฐ ะฒะฐัˆะต ัƒั‚ะฒะตั€ะถะดะตะฝะธะต, ะฐ ะฝะต ะฒะฝะพัะธั‚ ะธั… ัะฐะผ. ะ”ะพ ัƒั‡ั‘ั‚ะฝั‹ั… ะดะฐะฝะฝั‹ั…, ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน ะธ ะฝะฐัั‚ั€ะพะตะบ ะพะฝ ะฝะต ะดะพะฑะตั€ั‘ั‚ัั ะฝะธะบะพะณะดะฐ. ะะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ั‹ ะผะพะณัƒั‚ ะพัั‚ะฐะฒะธั‚ัŒ ะตะณะพ ะฒั‹ะบะปัŽั‡ะตะฝะฝั‹ะผ ะดะปั ะฒัะตะน ัƒัั‚ะฐะฝะพะฒะบะธ, ะฐ ะฒั‹ ะผะพะถะตั‚ะต ัะบั€ั‹ั‚ัŒ ะตะณะพ ะตั‰ั‘ ะฟั€ะธ ะฟะตั€ะฒะธั‡ะฝะพะน ะฝะฐัั‚ั€ะพะนะบะต. -**ะŸะพัะปะตะดะพะฒะฐั‚ะตะปัŒะฝั‹ะต ะฟะพะดะบะปัŽั‡ะตะฝะธั:** -ะŸะพะดะบะปัŽั‡ะตะฝะธะต ะบ ะฟะพัะปะตะดะพะฒะฐั‚ะตะปัŒะฝั‹ะผ ัƒัั‚ั€ะพะนัั‚ะฒะฐะผ (ะผะฐั€ัˆั€ัƒั‚ะธะทะฐั‚ะพั€ั‹, ะบะพะผะผัƒั‚ะฐั‚ะพั€ั‹, ะผะธะบั€ะพะบะพะฝั‚ั€ะพะปะปะตั€ั‹ ะธ ั‚. ะด.) ะฝะฐะฟั€ัะผัƒัŽ ะธะท ะฑั€ะฐัƒะทะตั€ะฐ ะธะปะธ ะฟั€ะธะปะพะถะตะฝะธั ะดะปั ั€ะฐะฑะพั‡ะตะณะพ ัั‚ะพะปะฐ. ะะฐัั‚ั€ะพะนะบะฐ ัะบะพั€ะพัั‚ะธ ะฟะตั€ะตะดะฐั‡ะธ ะดะฐะฝะฝั‹ั…, ะฑะธั‚ะพะฒ ะดะฐะฝะฝั‹ั…, ัั‚ะพะฟ-ะฑะธั‚ะพะฒ ะธ ั‡ั‘ั‚ะฝะพัั‚ะธ. ะ˜ัะฟะพะปัŒะทัƒะตั‚ Web Serial API ะฒ ะฟะพะดะดะตั€ะถะธะฒะฐะตะผั‹ั… ะฑั€ะฐัƒะทะตั€ะฐั… ะธะปะธ ะฝะฐั‚ะธะฒะฝั‹ะน ะฑัะบะตะฝะด ะฒ ะฟั€ะธะปะพะถะตะฝะธะธ Electron. +**ะ’ั…ะพะด ะธ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะธ:** +ะ›ะพะบะฐะปัŒะฝั‹ะต ัƒั‡ั‘ั‚ะฝั‹ะต ะทะฐะฟะธัะธ, ะฐ ั‚ะฐะบะถะต ะฒั…ะพะด ั‡ะตั€ะตะท OIDC, LDAP, GitHub ะธ Google, ั ะดะฒัƒั…ั„ะฐะบั‚ะพั€ะฝะพะน ะฟั€ะพะฒะตั€ะบะพะน (TOTP), ะบะปัŽั‡ะฐะผะธ ะดะพัั‚ัƒะฟะฐ (WebAuthn) ะธ ะดะพะฒะตั€ะตะฝะฝั‹ะผะธ ัƒัั‚ั€ะพะนัั‚ะฒะฐะผะธ. ะะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ั‹ ะผะพะณัƒั‚ ัƒะฟั€ะฐะฒะปัั‚ัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัะผะธ, ัะพะฟะพัั‚ะฐะฒะปัั‚ัŒ ะณั€ัƒะฟะฟั‹ OIDC ั ั€ะพะปัะผะธ, ะฒะธะดะตั‚ัŒ ะฒัะต ะฐะบั‚ะธะฒะฝั‹ะต ัะตััะธะธ ะฝะฐ ะฒัะตั… ะฟะปะฐั‚ั„ะพั€ะผะฐั… ะธ ะทะฐะฒะตั€ัˆะฐั‚ัŒ ะธั…. ะกะฒัะถะธั‚ะต ะปะพะบะฐะปัŒะฝัƒัŽ ัƒั‡ั‘ั‚ะฝัƒัŽ ะทะฐะฟะธััŒ ั OIDC ะธ ัะผะพั‚ั€ะธั‚ะต ะถัƒั€ะฝะฐะป ะฐัƒะดะธั‚ะฐ ะดะตะนัั‚ะฒะธะน ะบะฐะถะดะพะณะพ. +**ะ ะพะปะธ ะธ ะพะฑั‰ะธะน ะดะพัั‚ัƒะฟ:** +ะกะพะทะดะฐะฒะฐะนั‚ะต ั€ะพะปะธ ะธ ะดะตะปะธั‚ะตััŒ ั…ะพัั‚ะฐะผะธ ั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัะผะธ ะธะปะธ ั€ะพะปัะผะธ ะฝะฐ ั‡ะตั‚ั‹ั€ั‘ั… ัƒั€ะพะฒะฝัั…: ะฟะพะดะบะปัŽั‡ะตะฝะธะต, ะฟั€ะพัะผะพั‚ั€, ะธะทะผะตะฝะตะฝะธะต ะธ ัƒะฟั€ะฐะฒะปะตะฝะธะต. ะ ะฐะฑะพั‚ะฐะตั‚ ัะพ ะฒัะตะผะธ ัะฟะพัะพะฑะฐะผะธ ะฐัƒั‚ะตะฝั‚ะธั„ะธะบะฐั†ะธะธ ะธ ะฒัะตะผะธ ะฟั€ะพั‚ะพะบะพะปะฐะผะธ, ะฐ ัƒั‡ั‘ั‚ะฝั‹ะต ะดะฐะฝะฝั‹ะต ะดะปั ะพะฑั‰ะตะณะพ ั…ะพัั‚ะฐ ะผะพะถะฝะพ ะฟะตั€ะตะพะฟั€ะตะดะตะปะธั‚ัŒ. + + + + + + **ะžะฟะพะฒะตั‰ะตะฝะธั:** -ะะฐัั‚ั€ะพะนั‚ะต ะฟั€ะฐะฒะธะปะฐ ะพะฟะพะฒะตั‰ะตะฝะธะน ะฝะฐ ะพัะฝะพะฒะต ะฟะพั€ะพะณะพะฒั‹ั… ะทะฝะฐั‡ะตะฝะธะน ะดะปั ะผะตั‚ั€ะธะบ ั…ะพัั‚ะฐ (CPU, ะฟะฐะผัั‚ัŒ, ะดะธัะบ ะธ ั‚. ะด.) ะธ ะฟะพะปัƒั‡ะฐะนั‚ะต ัƒะฒะตะดะพะผะปะตะฝะธั ั‡ะตั€ะตะท ntfy ะธะปะธ ะฒะตะฑั…ัƒะบะธ ะฟั€ะธ ะธั… ัั€ะฐะฑะฐั‚ั‹ะฒะฐะฝะธะธ. ะŸั€ะพัะผะฐั‚ั€ะธะฒะฐะนั‚ะต ะฐะบั‚ะธะฒะฝั‹ะต ะธ ั€ะฐะทั€ะตัˆั‘ะฝะฝั‹ะต ะพะฟะพะฒะตั‰ะตะฝะธั ะฒ ะถัƒั€ะฝะฐะปะต ะธัั‚ะพั€ะธะธ. +ะ—ะฐะดะฐะนั‚ะต ะฟั€ะฐะฒะธะปะฐ ะฟะพ ะผะตั‚ั€ะธะบะฐะผ ั…ะพัั‚ะพะฒ, ะฝะฐะฟั€ะธะผะตั€ ะฟั€ะพั†ะตััะพั€ัƒ, ะฟะฐะผัั‚ะธ ะธ ะดะธัะบัƒ, ะธ ะฟะพะปัƒั‡ะฐะนั‚ะต ัƒะฒะตะดะพะผะปะตะฝะธั ั‡ะตั€ะตะท ntfy, Discord ะธะปะธ webhook, ะบะพะณะดะฐ ะพะฝะธ ัั€ะฐะฑะฐั‚ั‹ะฒะฐัŽั‚. ะกะผะพั‚ั€ะธั‚ะต ะฐะบั‚ะธะฒะฝั‹ะต ะธ ัƒะถะต ัะฝัั‚ั‹ะต ะพะฟะพะฒะตั‰ะตะฝะธั ะฒ ะถัƒั€ะฝะฐะปะต ะธ ัƒะฑะธั€ะฐะนั‚ะต ั‚ะต, ั‡ั‚ะพ ะฒะฐะผ ะฝะต ะฝัƒะถะฝั‹. - - **ะ”ะพะผะฐัˆะฝัั ัั‚ั€ะฐะฝะธั†ะฐ:** -ะŸะพะปะฝะพัั‚ัŒัŽ ะฝะฐัั‚ั€ะฐะธะฒะฐะตะผะฐั ะดะพะผะฐัˆะฝัั ัั‚ั€ะฐะฝะธั†ะฐ ั ัะตั‚ะบะพะน ะฒะธะดะถะตั‚ะพะฒ ั ะฟะตั€ะตั‚ะฐัะบะธะฒะฐะฝะธะตะผ. ะ”ะพะฑะฐะฒะปัะนั‚ะต ะฒะธะดะถะตั‚ั‹ ะดะปั ัั‚ะฐั‚ัƒัะฐ ั…ะพัั‚ะฐ, ััั‹ะปะพะบ ะฝะฐ ัะตั€ะฒะธัั‹, ั‡ะฐัะพะฒ, ะทะฐะผะตั‚ะพะบ, RSS-ะปะตะฝั‚, ะฟะพะณะพะดั‹, ะบะพะฝั‚ะตะนะฝะตั€ะพะฒ Docker, ะณั€ะฐั„ะธะบะพะฒ ะผะตั‚ั€ะธะบ ั…ะพัั‚ะฐ, ะฒัั‚ั€ะพะตะฝะฝั‹ั… ั‚ะตั€ะผะธะฝะฐะปะพะฒ, iframe ะธ ะผะฝะพะณะพะณะพ ะดั€ัƒะณะพะณะพ. - - - - -**ะจะธั„ั€ะพะฒะฐะฝะธะต ะฑะฐะทั‹ ะดะฐะฝะฝั‹ั…:** -ะ‘ัะบะตะฝะด ั…ั€ะฐะฝะธั‚ัั ะฒ ะฒะธะดะต ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝั‹ั… ั„ะฐะนะปะพะฒ ะฑะฐะทั‹ ะดะฐะฝะฝั‹ั… SQLite. ะŸะพะดั€ะพะฑะฝะตะต ะฒ [ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ](https://docs.termix.site/security). +ะกะตั‚ะบะฐ ะฒะธะดะถะตั‚ะพะฒ, ะบะพั‚ะพั€ัƒัŽ ะฒั‹ ัะพะฑะธั€ะฐะตั‚ะต ัะฐะผะธ ะฟะตั€ะตั‚ะฐัะบะธะฒะฐะฝะธะตะผ. ะ•ัั‚ัŒ ะฒะธะดะถะตั‚ั‹ ะดะปั ัะพัั‚ะพัะฝะธั ั…ะพัั‚ะพะฒ, ะฟะธะฝะณะพะฒ, ััั‹ะปะพะบ ะฝะฐ ัะตั€ะฒะธัั‹, ะทะฐะบะปะฐะดะพะบ, ะฟะพะธัะบะฐ, ั‡ะฐัะพะฒ, ะบะฐะปะตะฝะดะฐั€ะตะน, ะพะฑั€ะฐั‚ะฝะพะณะพ ะพั‚ัั‡ั‘ั‚ะฐ, ะทะฐะผะตั‚ะพะบ, RSS, ะฟะพะณะพะดั‹, ะธะทะพะฑั€ะฐะถะตะฝะธะน, ะฒัั‚ั€ะพะตะฝะฝั‹ั… ัั‚ั€ะฐะฝะธั†, Docker, ั‚ัƒะฝะฝะตะปะตะน, ะณั€ะฐั„ะธะบะพะฒ ะผะตั‚ั€ะธะบ, ัะฒะพะธั… API ะธ ะดะฐะถะต ะถะธะฒะพะณะพ ั‚ะตั€ะผะธะฝะฐะปะฐ. -**ะกะตั‚ะตะฒะพะน ะณั€ะฐั„:** -ะะฐัั‚ั€ะพะนั‚ะต ะฟะฐะฝะตะปัŒ ัƒะฟั€ะฐะฒะปะตะฝะธั ะดะปั ะฒะธะทัƒะฐะปะธะทะฐั†ะธะธ ะฒะฐัˆะตะน ะดะพะผะฐัˆะฝะตะน ะปะฐะฑะพั€ะฐั‚ะพั€ะธะธ ะฝะฐ ะพัะฝะพะฒะต SSH-ะฟะพะดะบะปัŽั‡ะตะฝะธะน ั ะฟะพะดะดะตั€ะถะบะพะน ัั‚ะฐั‚ัƒัะพะฒ. +**ะกะฝะธะฟะฟะตั‚ั‹ ะธ ะธะฝัั‚ั€ัƒะผะตะฝั‚ั‹:** +ะกะพั…ั€ะฐะฝัะนั‚ะต ะบะพะผะฐะฝะดั‹, ะบะพั‚ะพั€ั‹ะต ั‡ะฐัั‚ะพ ะฝะฐะฑะธั€ะฐะตั‚ะต, ะธ ะทะฐะฟัƒัะบะฐะนั‚ะต ะธั… ะพะดะฝะธะผ ะฝะฐะถะฐั‚ะธะตะผ, ั ะฟะตั€ะตะผะตะฝะฝั‹ะผะธ ะดะปั ั…ะพัั‚ะฐ ะธ ะดะปั ัะพะฑัั‚ะฒะตะฝะฝะพะณะพ ะฒะฒะพะดะฐ. ะ’ั‹ะฟะพะปะฝัะนั‚ะต ะพะดะฝัƒ ะบะพะผะฐะฝะดัƒ ัั€ะฐะทัƒ ะฒะพ ะฒัะตั… ะพั‚ะบั€ั‹ั‚ั‹ั… ั‚ะตั€ะผะธะฝะฐะปะฐั… ะธ ะธั‰ะธั‚ะต ะฟะพ ะธัั‚ะพั€ะธะธ ะบะพะผะฐะฝะด ั ะฐะฒั‚ะพะดะพะฟะพะปะฝะตะฝะธะตะผ. -**ะ˜ะฝัั‚ั€ัƒะผะตะฝั‚ั‹ SSH:** -ะกะพะทะดะฐะฝะธะต ะฟะตั€ะตะธัะฟะพะปัŒะทัƒะตะผั‹ั… ั„ั€ะฐะณะผะตะฝั‚ะพะฒ ะบะพะผะฐะฝะด, ะฒั‹ะฟะพะปะฝัะตะผั‹ั… ะพะดะฝะธะผ ะฝะฐะถะฐั‚ะธะตะผ. ะ—ะฐะฟัƒัะบ ะพะดะฝะพะน ะบะพะผะฐะฝะดั‹ ะพะดะฝะพะฒั€ะตะผะตะฝะฝะพ ะฒ ะฝะตัะบะพะปัŒะบะธั… ะพั‚ะบั€ั‹ั‚ั‹ั… ั‚ะตั€ะผะธะฝะฐะปะฐั…. +**ะžะฑั‰ะธะน ะดะพัั‚ัƒะฟ ะบ ัะตััะธะธ:** +ะ”ะตะปะธั‚ะตััŒ ะถะธะฒะพะน ัะตััะธะตะน ั‚ะตั€ะผะธะฝะฐะปะฐ, RDP, VNC ะธะปะธ Telnet. ะžั‚ะฟั€ะฐะฒัŒั‚ะต ััั‹ะปะบัƒ, ะฟะพ ะบะพั‚ะพั€ะพะน ะผะพะถะฝะพ ะฟะพะดะบะปัŽั‡ะธั‚ัŒัั ะฑะตะท ัƒั‡ั‘ั‚ะฝะพะน ะทะฐะฟะธัะธ, ะธะปะธ ะฟะพะดะตะปะธั‚ะตััŒ ั ะบะพะฝะบั€ะตั‚ะฝั‹ะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะผ Termix, ั‚ะพะปัŒะบะพ ะดะปั ะฟั€ะพัะผะพั‚ั€ะฐ ะธะปะธ ั ะฟั€ะฐะฒะพะผ ะฒะฒะพะดะฐ. ะ”ะพัั‚ัƒะฟ ะผะพะถะตั‚ ะธัั‚ะตะบะฐั‚ัŒ ัะฐะผ ะธะปะธ ะพั‚ะทั‹ะฒะฐั‚ัŒัั ะฒ ะปัŽะฑะพะน ะผะพะผะตะฝั‚, ะธ ะตะณะพ ะผะพะถะฝะพ ะพั‚ะบะปัŽั‡ะธั‚ัŒ ะฟะพะปะฝะพัั‚ัŒัŽ ะธะปะธ ะดะปั ะพั‚ะดะตะปัŒะฝะพะณะพ ั…ะพัั‚ะฐ. -**ะŸะพัั‚ะพัะฝะฝั‹ะต ะฒะบะปะฐะดะบะธ:** -SSH-ัะตััะธะธ ะธ ะฒะบะปะฐะดะบะธ ะพัั‚ะฐัŽั‚ัั ะพั‚ะบั€ั‹ั‚ั‹ะผะธ ะฝะฐ ะฒัะตั… ัƒัั‚ั€ะพะนัั‚ะฒะฐั…/ะฟั€ะธ ะพะฑะฝะพะฒะปะตะฝะธะธ ัั‚ั€ะฐะฝะธั†ั‹, ะตัะปะธ ะฒะบะปัŽั‡ะตะฝะพ ะฒ ะฟั€ะพั„ะธะปะต ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั. +**ะ—ะฐะฟะธััŒ ัะตััะธะน ะธ ะถัƒั€ะฝะฐะปั‹:** +ะ—ะฐะฟะธัั‹ะฒะฐะนั‚ะต ัะตััะธะธ ั‚ะตั€ะผะธะฝะฐะปะฐ, RDP ะธ VNC ะธ ะฟั€ะพัะผะฐั‚ั€ะธะฒะฐะนั‚ะต ะธั… ะฟะพะทะถะต. ะกะบะฐั‡ะธะฒะฐะนั‚ะต ั‚ะตะบัั‚ะพะฒั‹ะต ะถัƒั€ะฝะฐะปั‹ ัะตััะธะธ ะธ ะทะฐะณะปัะดั‹ะฒะฐะนั‚ะต ะฒ ะถัƒั€ะฝะฐะป ะฟะพะดะบะปัŽั‡ะตะฝะธั, ั‡ั‚ะพะฑั‹ ัƒะฒะธะดะตั‚ัŒ, ั‡ั‚ะพ ะธะผะตะฝะฝะพ ะฟั€ะพะธัั…ะพะดะธะปะพ ะฒะพ ะฒั€ะตะผั ัะพะตะดะธะฝะตะฝะธั. + + + + +**ะŸะพัะปะตะดะพะฒะฐั‚ะตะปัŒะฝั‹ะต ะฟะพะดะบะปัŽั‡ะตะฝะธั:** +ะžะฑั‰ะฐะนั‚ะตััŒ ั ะฟะพัะปะตะดะพะฒะฐั‚ะตะปัŒะฝั‹ะผะธ ัƒัั‚ั€ะพะนัั‚ะฒะฐะผะธ ะฒั€ะพะดะต ะผะฐั€ัˆั€ัƒั‚ะธะทะฐั‚ะพั€ะพะฒ, ะบะพะผะผัƒั‚ะฐั‚ะพั€ะพะฒ ะธ ะผะธะบั€ะพะบะพะฝั‚ั€ะพะปะปะตั€ะพะฒ ะธะท ะฑั€ะฐัƒะทะตั€ะฐ ะธะปะธ ะฝะฐัั‚ะพะปัŒะฝะพะณะพ ะฟั€ะธะปะพะถะตะฝะธั. ะะฐัั‚ั€ะฐะธะฒะฐะนั‚ะต ัะบะพั€ะพัั‚ัŒ, ะฑะธั‚ั‹ ะดะฐะฝะฝั‹ั…, ัั‚ะพะฟะพะฒั‹ะต ะฑะธั‚ั‹ ะธ ั‡ั‘ั‚ะฝะพัั‚ัŒ. ะ’ ะฟะพะดั…ะพะดัั‰ะธั… ะฑั€ะฐัƒะทะตั€ะฐั… ะธัะฟะพะปัŒะทัƒะตั‚ัั Web Serial API, ะฐ ะฒ ะฝะฐัั‚ะพะปัŒะฝะพะผ ะฟั€ะธะปะพะถะตะฝะธะธ ัะพะฑัั‚ะฒะตะฝะฝั‹ะน ะฑัะบะตะฝะด. + + + + + + +**Tailscale:** +ะŸะพะดั‚ัะณะธะฒะฐะนั‚ะต ัƒัั‚ั€ะพะนัั‚ะฒะฐ ะธะท ัะฒะพะตะน tailnet, ั‡ั‚ะพะฑั‹ ะดะพะฑะฐะฒะธั‚ัŒ ะธั… ะบะฐะบ ั…ะพัั‚ั‹ ะฒ ะฟะฐั€ัƒ ะฝะฐะถะฐั‚ะธะน, ะธ ะฟะพะดะบะปัŽั‡ะฐะนั‚ะตััŒ ั‡ะตั€ะตะท Tailscale SSH: ะดะพัั‚ัƒะฟะพะผ ะทะฐะนะผัƒั‚ัั ะฟั€ะฐะฒะธะปะฐ tailnet, ะฐ ัƒั‡ั‘ั‚ะฝั‹ะต ะดะฐะฝะฝั‹ะต ั…ั€ะฐะฝะธั‚ัŒ ะฝะต ะฟั€ะธะดั‘ั‚ัั. Headscale ะธ ัะฒะพะธ ะฐะดั€ะตัะฐ ั‚ะพะถะต ั€ะฐะฑะพั‚ะฐัŽั‚. + + + + +**Proxmox:** +ะ˜ะผะฟะพั€ั‚ะธั€ัƒะนั‚ะต ั…ะพัั‚ั‹ ะฟั€ัะผะพ ะธะท ัƒัั‚ะฐะฝะพะฒะบะธ Proxmox ะธ ัะปะตะดะธั‚ะต ะทะฐ ะฟะพะบะฐะทะฐั‚ะตะปัะผะธ ัƒะทะปะพะฒ ะธ ะณะพัั‚ะตะฒั‹ั… ะผะฐัˆะธะฝ, ะฒะบะปัŽั‡ะฐั ะฟั€ะพั†ะตััะพั€, ะฟะฐะผัั‚ัŒ ะธ ั…ั€ะฐะฝะธะปะธั‰ะต, ะฝะฐ ะพั‚ะดะตะปัŒะฝะพะน ะฒะบะปะฐะดะบะต. + + + + + + +**ะ ะฐะฑะพั‡ะธะต ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ ะธ ะฒะบะปะฐะดะบะธ:** +ะกะพั…ั€ะฐะฝะธั‚ะต ะฝะฐะฑะพั€ ะฒะบะปะฐะดะพะบ ะฒะผะตัั‚ะต ั ั€ะฐะทะดะตะปะตะฝะธะตะผ ัะบั€ะฐะฝะฐ ะธ ะพั‚ะบั€ะพะนั‚ะต ะฒัั‘ ัั‚ะพ ะพะดะฝะธะผ ะฝะฐะถะฐั‚ะธะตะผ. Termix ะฟะพะผะฝะธั‚ ะธ ะฟะพัะปะตะดะฝัŽัŽ ัะตััะธัŽ, ะฟะพัั‚ะพะผัƒ ะฒะบะปะฐะดะบะธ ะฒะพะทะฒั€ะฐั‰ะฐัŽั‚ัั ะฟะพัะปะต ะพะฑะฝะพะฒะปะตะฝะธั ัั‚ั€ะฐะฝะธั†ั‹ ะธ ะฝะฐ ะดั€ัƒะณะธั… ัƒัั‚ั€ะพะนัั‚ะฒะฐั…. + + + + +**ะŸะพัˆะฐะณะพะฒะฐั ะฝะฐัั‚ั€ะพะนะบะฐ:** +ะšะพั€ะพั‚ะบะฐั ะฝะฐัั‚ั€ะพะนะบะฐ ะฟะพะผะพะถะตั‚ ะฒั‹ะฑั€ะฐั‚ัŒ ัˆะฐะฑะปะพะฝ ะธะฝั‚ะตั€ั„ะตะนัะฐ, ั‚ะตะผัƒ, ะฝัƒะถะฝั‹ะต ะฒะพะทะผะพะถะฝะพัั‚ะธ ะธ ะฟะตั€ะฒั‹ะน ั…ะพัั‚. ะŸั€ะพัั‚ะพะน ั€ะตะถะธะผ ัะบั€ั‹ะฒะฐะตั‚ ั‚ะพ, ั‡ะตะผ ะฒั‹ ะฝะต ะฟะพะปัŒะทัƒะตั‚ะตััŒ, ะฐ ะฝะฐัั‚ั€ะพะนะบัƒ ะผะพะถะฝะพ ะฟั€ะพะนั‚ะธ ะทะฐะฝะพะฒะพ ะธะปะธ ัะผะตะฝะธั‚ัŒ ัˆะฐะฑะปะพะฝ ะฒ ะปัŽะฑะพะน ะผะพะผะตะฝั‚. + + + + + + +**ะะฒั‚ะพะฝะพะผะฝั‹ะน ะบะปะธะตะฝั‚ ะธ ัะธะฝั…ั€ะพะฝะธะทะฐั†ะธั:** +ะะฐัั‚ะพะปัŒะฝะพะต ะฟั€ะธะปะพะถะตะฝะธะต ั€ะฐะฑะพั‚ะฐะตั‚ ัะฐะผะพ ะฟะพ ัะตะฑะต, ัะพ ัะฒะพะธะผ ะฑัะบะตะฝะดะพะผ ะธ ะฑะฐะทะพะน ะดะฐะฝะฝั‹ั…, ะฑะตะท ัะตั€ะฒะตั€ะฐ. ะ•ะณะพ ะผะพะถะฝะพ ะฟะพะดะบะปัŽั‡ะธั‚ัŒ ะบ ัะตั€ะฒะตั€ัƒ Termix, ั‡ั‚ะพะฑั‹ ะฒ ะพะฑะต ัั‚ะพั€ะพะฝั‹ ัะธะฝั…ั€ะพะฝะธะทะธั€ะพะฒะฐั‚ัŒ ั…ะพัั‚ั‹, ัƒั‡ั‘ั‚ะฝั‹ะต ะดะฐะฝะฝั‹ะต, ัะฝะธะฟะฟะตั‚ั‹ ะธ ะพัั‚ะฐะปัŒะฝะพะต, ะธ ะฒั‹ะฑั€ะฐั‚ัŒ, ะพั‚ะบัƒะดะฐ ะธะดัƒั‚ ะฟะพะดะบะปัŽั‡ะตะฝะธั: ั ะฒะฐัˆะตะณะพ ะบะพะผะฟัŒัŽั‚ะตั€ะฐ ะธะปะธ ั‡ะตั€ะตะท ัะตั€ะฒะตั€. + + + + +**ะšะพะผะฐะฝะดะฝะฐั ัั‚ั€ะพะบะฐ:** +CLI `termix` ะดะปั ะฒะฐัˆะตะน ะพะฑะพะปะพั‡ะบะธ ะธ ะฒะฐัˆะธั… ัะบั€ะธะฟั‚ะพะฒ. ะžั‚ะบั€ั‹ะฒะฐะนั‚ะต ั‚ะตั€ะผะธะฝะฐะปั‹, ะฒั‹ะฟะพะปะฝัะนั‚ะต ะบะพะผะฐะฝะดัƒ ะฝะฐ ะพะดะฝะพะผ ั…ะพัั‚ะต ะธะปะธ ะฝะฐ ั†ะตะปะพะผ ั„ะปะพั‚ะต, ะฟะตั€ะตะผะตั‰ะฐะนั‚ะต ั„ะฐะนะปั‹ ะฟะพ SFTP ะธ ัƒะฟั€ะฐะฒะปัะนั‚ะต ั…ะพัั‚ะฐะผะธ, ัะฝะธะฟะฟะตั‚ะฐะผะธ ะธ ัƒั‡ั‘ั‚ะฝั‹ะผะธ ะดะฐะฝะฝั‹ะผะธ. ะฃัั‚ะฐะฝะพะฒะธั‚ะต ั‡ะตั€ะตะท `npm install -g @termix-cli/cli` ะธะปะธ ะฒะพะทัŒะผะธั‚ะต ะพั‚ะดะตะปัŒะฝั‹ะน ะธัะฟะพะปะฝัะตะผั‹ะน ั„ะฐะนะป. ะกะผะพั‚ั€ะธั‚ะต [ะดะพะบัƒะผะตะฝั‚ะฐั†ะธัŽ CLI](https://docs.termix.site/cli). + + + + + + +**ะ‘ะตะทะพะฟะฐัะฝะพัั‚ัŒ:** +ะŸะฐั€ะพะปะธ, ะบะปัŽั‡ะธ ะธ ะดั€ัƒะณะธะต ัะตะบั€ะตั‚ั‹ ัˆะธั„ั€ัƒัŽั‚ัั ะดะปั ะบะฐะถะดะพะณะพ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั, ะฐ ัะฐะผะธ ั„ะฐะนะปั‹ ะฑะฐะทั‹ ะดะฐะฝะฝั‹ั… ะผะพะถะฝะพ ะทะฐัˆะธั„ั€ะพะฒะฐั‚ัŒ ะฝะฐ ะดะธัะบะต. ะšะฐะบ ัั‚ะพ ัƒัั‚ั€ะพะตะฝะพ, ะพะฟะธัะฐะฝะพ ะฒ [ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ](https://docs.termix.site/security). **ะฏะทั‹ะบะธ:** -ะ’ัั‚ั€ะพะตะฝะฝะฐั ะฟะพะดะดะตั€ะถะบะฐ ะพะบะพะปะพ 30 ัะทั‹ะบะพะฒ (ัƒะฟั€ะฐะฒะปัะตั‚ัั ั‡ะตั€ะตะท [Crowdin](https://docs.termix.site/translations)). +ะžะบะพะปะพ 30 ะฒัั‚ั€ะพะตะฝะฝั‹ั… ัะทั‹ะบะพะฒ, ะบะพั‚ะพั€ั‹ะต ะฒะตะดัƒั‚ัั ั‡ะตั€ะตะท [Crowdin](https://docs.termix.site/translations). @@ -196,20 +252,23 @@ SSH-ัะตััะธะธ ะธ ะฒะบะปะฐะดะบะธ ะพัั‚ะฐัŽั‚ัั ะพั‚ะบั€ั‹ั‚ั‹ะผะธ ะฝะฐ ะฒั
-ะ‘ะพะปัŒัˆะต ะฒะพะทะผะพะถะฝะพัั‚ะตะน +ะ”ั€ัƒะณะธะต ะฒะพะทะผะพะถะฝะพัั‚ะธ
-- **ะŸะฐะฝะตะปัŒ ัƒะฟั€ะฐะฒะปะตะฝะธั** - ะŸั€ะพัะผะพั‚ั€ ะธะฝั„ะพั€ะผะฐั†ะธะธ ะพ ัะตั€ะฒะตั€ะต ะฝะฐ ะฟะฐะฝะตะปะธ ัƒะฟั€ะฐะฒะปะตะฝะธั ะพะดะฝะธะผ ะฒะทะณะปัะดะพะผ -- **API-ะบะปัŽั‡ะธ** - ะกะพะทะดะฐะฝะธะต API-ะบะปัŽั‡ะตะน ั ะพะฑะปะฐัั‚ัŒัŽ ะฒะธะดะธะผะพัั‚ะธ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะธ ัั€ะพะบะฐะผะธ ะดะตะนัั‚ะฒะธั ะดะปั ะธัะฟะพะปัŒะทะพะฒะฐะฝะธั ะฒ ะฐะฒั‚ะพะผะฐั‚ะธะทะฐั†ะธะธ/CI -- **ะญะบัะฟะพั€ั‚/ะธะผะฟะพั€ั‚ ะดะฐะฝะฝั‹ั…** - ะญะบัะฟะพั€ั‚ ะธ ะธะผะฟะพั€ั‚ SSH-ั…ะพัั‚ะพะฒ, ัƒั‡ั‘ั‚ะฝั‹ั… ะดะฐะฝะฝั‹ั… ะธ ะดะฐะฝะฝั‹ั… ั„ะฐะนะปะพะฒะพะณะพ ะผะตะฝะตะดะถะตั€ะฐ -- **ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะฐั ะฝะฐัั‚ั€ะพะนะบะฐ SSL** - ะ’ัั‚ั€ะพะตะฝะฝะฐั ะณะตะฝะตั€ะฐั†ะธั ะธ ัƒะฟั€ะฐะฒะปะตะฝะธะต SSL-ัะตั€ั‚ะธั„ะธะบะฐั‚ะฐะผะธ ั ะฟะตั€ะตะฝะฐะฟั€ะฐะฒะปะตะฝะธะตะผ ะฝะฐ HTTPS -- **ะกะพะฒั€ะตะผะตะฝะฝั‹ะน ะธะฝั‚ะตั€ั„ะตะนั** - ะงะธัั‚ั‹ะน ะธะฝั‚ะตั€ั„ะตะนั ะดะปั ะดะตัะบั‚ะพะฟะฐ ะธ ะผะพะฑะธะปัŒะฝั‹ั… ัƒัั‚ั€ะพะนัั‚ะฒ, ะฟะพัั‚ั€ะพะตะฝะฝั‹ะน ะฝะฐ React, Tailwind CSS ะธ Shadcn. ะ’ั‹ะฑะพั€ ะผะตะถะดัƒ ะผะฝะพะถะตัั‚ะฒะพะผ ั€ะฐะทะปะธั‡ะฝั‹ั… ั‚ะตะผ ะธะฝั‚ะตั€ั„ะตะนัะฐ, ะฒะบะปัŽั‡ะฐั ัะฒะตั‚ะปัƒัŽ, ั‚ั‘ะผะฝัƒัŽ, Dracula ะธ ั‚. ะด. ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต URL-ะผะฐั€ัˆั€ัƒั‚ะพะฒ ะดะปั ะพั‚ะบั€ั‹ั‚ะธั ะปัŽะฑะพะณะพ ะฟะพะดะบะปัŽั‡ะตะฝะธั ะฒ ะฟะพะปะฝะพัะบั€ะฐะฝะฝะพะผ ั€ะตะถะธะผะต. -- **ะ˜ัั‚ะพั€ะธั ะบะพะผะฐะฝะด** - ะะฒั‚ะพะดะพะฟะพะปะฝะตะฝะธะต ะธ ะฟั€ะพัะผะพั‚ั€ ั€ะฐะฝะตะต ะฒั‹ะฟะพะปะฝะตะฝะฝั‹ั… SSH-ะบะพะผะฐะฝะด -- **ะ‘ั‹ัั‚ั€ะพะต ะฟะพะดะบะปัŽั‡ะตะฝะธะต** - ะŸะพะดะบะปัŽั‡ะตะฝะธะต ะบ ัะตั€ะฒะตั€ัƒ ะฑะตะท ะฝะตะพะฑั…ะพะดะธะผะพัั‚ะธ ัะพั…ั€ะฐะฝะตะฝะธั ะดะฐะฝะฝั‹ั… ะฟะพะดะบะปัŽั‡ะตะฝะธั -- **ะšะพะผะฐะฝะดะฝะฐั ะฟะฐะปะธั‚ั€ะฐ** - ะ”ะฒะพะนะฝะพะต ะฝะฐะถะฐั‚ะธะต ะปะตะฒะพะณะพ Shift ะดะปั ะฑั‹ัั‚ั€ะพะณะพ ะดะพัั‚ัƒะฟะฐ ะบ SSH-ะฟะพะดะบะปัŽั‡ะตะฝะธัะผ ั ะบะปะฐะฒะธะฐั‚ัƒั€ั‹ -- **ะ˜ะฝั‚ะตะณั€ะฐั†ะธั ั Proxmox** - ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะพะต ะดะพะฑะฐะฒะปะตะฝะธะต ั…ะพัั‚ะพะฒ ะฒ Termix ะธะท ะฒะฐัˆะตะณะพ ัะบะทะตะผะฟะปัั€ะฐ Proxmox -- **ะ‘ะพะณะฐั‚ั‹ะน ั„ัƒะฝะบั†ะธะพะฝะฐะป SSH** - ะŸะพะดะดะตั€ะถะบะฐ jump-ั…ะพัั‚ะพะฒ, Warpgate, ะฟะพะดะบะปัŽั‡ะตะฝะธะน ะฝะฐ ะพัะฝะพะฒะต TOTP, SOCKS5, ะฒะตั€ะธั„ะธะบะฐั†ะธะธ ะบะปัŽั‡ะตะน ั…ะพัั‚ะฐ, ะฐะฒั‚ะพะทะฐะฟะพะปะฝะตะฝะธั ะฟะฐั€ะพะปะตะน, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, ะปะพะณะธั€ะพะฒะฐะฝะธั ั‚ะตั€ะผะธะฝะฐะปะฐ, ะฟะตั€ะตะฐะดั€ะตัะฐั†ะธะธ SSH-ะฐะณะตะฝั‚ะฐ, SSH-ะฐะณะตะฝั‚ะฐ Bitwarden, ะฟะพะดะฟะธัะธ SSH ั‡ะตั€ะตะท HashiCorp Vault ะธ ะผะฝะพะณะพะณะพ ะดั€ัƒะณะพะณะพ. -- **Termix ID** - ะะฝะฐะปะพะณ sshid.io, ะฒัั‚ั€ะพะตะฝะฝั‹ะน ะฒ Termix. ะ—ะฐั€ะตะณะธัั‚ั€ะธั€ัƒะนั‚ะต ะธะผั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั, ะพะฟัƒะฑะปะธะบัƒะนั‚ะต ัะฒะพะธ ะฟัƒะฑะปะธั‡ะฝั‹ะต SSH-ะบะปัŽั‡ะธ ะฟะพ URL ั€ะตะทะพะปะฒะตั€ะฐ ะธ ะธัะฟะพะปัŒะทัƒะนั‚ะต ะฒัั‚ั€ะพะตะฝะฝั‹ะน ะฆะก ะดะปั ะฒั‹ะดะฐั‡ะธ SSH-ัะตั€ั‚ะธั„ะธะบะฐั‚ะพะฒ. +- **ะŸะฐะฝะตะปัŒ** - ะ’ะฐัˆะธ ัะตั€ะฒะตั€ั‹ ะพะดะฝะธะผ ะฒะทะณะปัะดะพะผ, ะบะฐั€ั‚ะพั‡ะบะธ ั€ะฐััั‚ะฐะฒะปัะตั‚ะต ะฒั‹ ัะฐะผะธ +- **ะกั…ะตะผะฐ ัะตั‚ะธ** - ะ’ะฐัˆะฐ ะดะพะผะฐัˆะฝัั ะปะฐะฑะพั€ะฐั‚ะพั€ะธั, ะฝะฐั€ะธัะพะฒะฐะฝะฝะฐั ะฟะพ ั…ะพัั‚ะฐะผ, ั ัะพัั‚ะพัะฝะธะตะผ ะฒ ั€ะตะฐะปัŒะฝะพะผ ะฒั€ะตะผะตะฝะธ +- **ะœะพะฝะธั‚ะพั€ tmux** - ะŸั€ะพัะผะพั‚ั€ ัะตััะธะน, ะพะบะพะฝ ะธ ะฟะฐะฝะตะปะตะน tmux ั ะฟั€ะตะดะฟั€ะพัะผะพั‚ั€ะพะผ ะธ ะฟะพะธัะบะพะผ +- **ะšะปัŽั‡ะธ API** - ะšะปัŽั‡ะธ ะดะปั ะบะพะฝะบั€ะตั‚ะฝะพะณะพ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ัะพ ัั€ะพะบะพะผ ะดะตะนัั‚ะฒะธั, ะดะปั ัะบั€ะธะฟั‚ะพะฒ ะธ CI +- **ะญะบัะฟะพั€ั‚ ะธ ะธะผะฟะพั€ั‚** - ะŸะตั€ะตะฝะพั ั…ะพัั‚ะพะฒ, ัƒั‡ั‘ั‚ะฝั‹ั… ะดะฐะฝะฝั‹ั… ะธ ะดะฐะฝะฝั‹ั… ั„ะฐะนะปะพะฒะพะณะพ ะผะตะฝะตะดะถะตั€ะฐ +- **ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธะน SSL** - ะกะตั€ั‚ะธั„ะธะบะฐั‚ั‹ ะฒั‹ะฟัƒัะบะฐัŽั‚ัั ะธ ะพะฑะฝะพะฒะปััŽั‚ัั ะทะฐ ะฒะฐั, ั ะฟะตั€ะตั…ะพะดะพะผ ะฝะฐ HTTPS, ะปะธะฑะพ ะธัะฟะพะปัŒะทัƒะนั‚ะต ัะฒะพะธ +- **ะ‘ะฐะทั‹ ะดะฐะฝะฝั‹ั…** - ะŸะพ ัƒะผะพะปั‡ะฐะฝะธัŽ SQLite, ะฟะพะดะดะตั€ะถะธะฒะฐัŽั‚ัั ั‚ะฐะบะถะต PostgreSQL ะธ MySQL +- **ะกะพะฒั€ะตะผะตะฝะฝั‹ะน ะธะฝั‚ะตั€ั„ะตะนั** - ะะบะบัƒั€ะฐั‚ะฝั‹ะน ะธะฝั‚ะตั€ั„ะตะนั ะฝะฐ React ะดะปั ะบะพะผะฟัŒัŽั‚ะตั€ะฐ ะธ ั‚ะตะปะตั„ะพะฝะฐ, ั ั‚ะตะผะฐะผะธ ะฒั€ะพะดะต ัะฒะตั‚ะปะพะน, ั‚ั‘ะผะฝะพะน ะธ Dracula. ะ›ัŽะฑะพะต ะฟะพะดะบะปัŽั‡ะตะฝะธะต ะพั‚ะบั€ั‹ะฒะฐะตั‚ัั ะฝะฐ ะฒะตััŒ ัะบั€ะฐะฝ ะฟะพ ััั‹ะปะบะต +- **ะŸะฐะปะธั‚ั€ะฐ ะบะพะผะฐะฝะด** - ะ”ะฒะพะนะฝะพะต ะฝะฐะถะฐั‚ะธะต ะปะตะฒะพะณะพ Shift, ั‡ั‚ะพะฑั‹ ะฟะตั€ะตะนั‚ะธ ะบ ั…ะพัั‚ัƒ ั ะบะปะฐะฒะธะฐั‚ัƒั€ั‹ +- **ะกะพั‡ะตั‚ะฐะฝะธั ะบะปะฐะฒะธัˆ** - ะŸะตั€ะตั…ะพะด ะผะตะถะดัƒ ะฒะบะปะฐะดะบะฐะผะธ, ะธั… ะทะฐะบั€ั‹ั‚ะธะต ะธ ะดั€ัƒะณะพะต, ะฒัั‘ ะผะพะถะฝะพ ะฟะตั€ะตะฝะฐะทะฝะฐั‡ะธั‚ัŒ +- **Wake-on-LAN** - ะ ะฐะทะฑัƒะดะธั‚ะต ะผะฐัˆะธะฝัƒ ะธะท Termix ะธะปะธ ะธะท ัˆะฐะณะฐ ะฐะฒั‚ะพะผะฐั‚ะธะทะฐั†ะธะธ +- **ะ”ะพะฒะตั€ะตะฝะฝั‹ะน ะฟั€ะพะบัะธ** - ะŸัƒัั‚ัŒ ะพะฑั€ะฐั‚ะฝั‹ะน ะฟั€ะพะบัะธ ะฒะพะทัŒะผั‘ั‚ ะฒั…ะพะด ะฝะฐ ัะตะฑั ะธ ะฟะตั€ะตะดะฐัั‚ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั +- **ะ‘ะพะณะฐั‚ั‹ะต ะฒะพะทะผะพะถะฝะพัั‚ะธ SSH** - ะŸั€ะพะผะตะถัƒั‚ะพั‡ะฝั‹ะต ั…ะพัั‚ั‹, Warpgate, ะทะฐะฟั€ะพัั‹ TOTP, SOCKS5, ะฟั€ะพะฒะตั€ะบะฐ ะบะปัŽั‡ะตะน ั…ะพัั‚ะฐ, ะฐะฒั‚ะพะทะฐะฟะพะปะฝะตะฝะธะต ะฟะฐั€ะพะปะตะน, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, ะถัƒั€ะฝะฐะปั‹ ั‚ะตั€ะผะธะฝะฐะปะฐ, ะฟั€ะพะฑั€ะพั ะฐะณะตะฝั‚ะฐ, SSH-ะฐะณะตะฝั‚ Bitwarden, ะฟะพะดะฟะธััŒ SSH ั‡ะตั€ะตะท HashiCorp Vault ะธ ะดั€ัƒะณะพะต +- **Termix ID** - ะ’ัั‚ั€ะพะตะฝะฝั‹ะน ะฐะฝะฐะปะพะณ sshid.io. ะ—ะฐะนะผะธั‚ะต ะธะผั, ะพะฟัƒะฑะปะธะบัƒะนั‚ะต ะพั‚ะบั€ั‹ั‚ั‹ะต ะบะปัŽั‡ะธ ะฟะพ ะฐะดั€ะตััƒ ั€ะฐัะฟะพะทะฝะฐะฒะฐั‚ะตะปั ะธ ะฒั‹ะฟัƒัะบะฐะนั‚ะต SSH-ัะตั€ั‚ะธั„ะธะบะฐั‚ั‹ ั‡ะตั€ะตะท ะฒัั‚ั€ะพะตะฝะฝั‹ะน ั†ะตะฝั‚ั€ ัะตั€ั‚ะธั„ะธะบะฐั†ะธะธ
@@ -220,7 +279,7 @@ SSH-ัะตััะธะธ ะธ ะฒะบะปะฐะดะบะธ ะพัั‚ะฐัŽั‚ัั ะพั‚ะบั€ั‹ั‚ั‹ะผะธ ะฝะฐ ะฒั - + @@ -252,9 +311,9 @@ SSH-ัะตััะธะธ ะธ ะฒะบะปะฐะดะบะธ ะพัั‚ะฐัŽั‚ัั ะพั‚ะบั€ั‹ั‚ั‹ะผะธ ะฝะฐ ะฒั ## ะฃัั‚ะฐะฝะพะฒะบะฐ -ะŸะพัะตั‚ะธั‚ะต [ะดะพะบัƒะผะตะฝั‚ะฐั†ะธัŽ](https://docs.termix.site/install) Termix ะดะปั ะฟะพะปัƒั‡ะตะฝะธั ะฟะพะปะฝั‹ั… ะธะฝัั‚ั€ัƒะบั†ะธะน ะฟะพ ัƒัั‚ะฐะฝะพะฒะบะต ะฝะฐ ะฒัะตั… ะฟะปะฐั‚ั„ะพั€ะผะฐั…. +ะŸะพะปะฝั‹ะต ะธะฝัั‚ั€ัƒะบั†ะธะธ ะฟะพ ัƒัั‚ะฐะฝะพะฒะบะต ะดะปั ะฒัะตั… ะฟะปะฐั‚ั„ะพั€ะผ ัะผะพั‚ั€ะธั‚ะต ะฒ [ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ Termix](https://docs.termix.site/install). -ะŸั€ะธะผะตั€ ั„ะฐะนะปะฐ Docker Compose (ะฒั‹ ะผะพะถะตั‚ะต ะพะฟัƒัั‚ะธั‚ัŒ `guacd` ะธ ัะตั‚ัŒ, ะตัะปะธ ะฝะต ะฟะปะฐะฝะธั€ัƒะตั‚ะต ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ั„ัƒะฝะบั†ะธะธ ัƒะดะฐะปะตะฝะฝะพะณะพ ั€ะฐะฑะพั‡ะตะณะพ ัั‚ะพะปะฐ): +ะŸั€ะธะผะตั€ ั„ะฐะนะปะฐ Docker Compose (`guacd` ะธ ัะตั‚ัŒ ะผะพะถะฝะพ ัƒะฑั€ะฐั‚ัŒ, ะตัะปะธ ัƒะดะฐะปั‘ะฝะฝั‹ะน ั€ะฐะฑะพั‡ะธะน ัั‚ะพะป ะฒะฐะผ ะฝะต ะฝัƒะถะตะฝ): ```yaml services: @@ -291,19 +350,45 @@ networks: driver: bridge ``` +### ะšะพะผะฐะฝะดะฝะฐั ัั‚ั€ะพะบะฐ + +ะฃ Termix ะตัั‚ัŒ ะธ CLI, ั‚ะฐะบ ั‡ั‚ะพ ัะตั€ะฒะตั€ะฐะผะธ ะผะพะถะฝะพ ัƒะฟั€ะฐะฒะปัั‚ัŒ ะธะท ั‚ะตั€ะผะธะฝะฐะปะฐ ะธ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ Termix ะฒ ัะฒะพะธั… ัะบั€ะธะฟั‚ะฐั…. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +ะžะฝ ัƒะผะตะตั‚ ะพั‚ะบั€ั‹ะฒะฐั‚ัŒ ั‚ะตั€ะผะธะฝะฐะปั‹, ะฒั‹ะฟะพะปะฝัั‚ัŒ ะบะพะผะฐะฝะดัƒ ะฝะฐ ะพะดะฝะพะผ ั…ะพัั‚ะต ะธะปะธ ะฝะฐ ั†ะตะปะพะผ ั„ะปะพั‚ะต, ะฟะตั€ะตะผะตั‰ะฐั‚ัŒ ั„ะฐะนะปั‹ ะฟะพ SFTP ะธ ัƒะฟั€ะฐะฒะปัั‚ัŒ ั…ะพัั‚ะฐะผะธ, ัะฝะธะฟะฟะตั‚ะฐะผะธ ะธ ัƒั‡ั‘ั‚ะฝั‹ะผะธ ะดะฐะฝะฝั‹ะผะธ. ะŸะพะปะฝะฐั ะดะพะบัƒะผะตะฝั‚ะฐั†ะธั ะตัั‚ัŒ ะฝะฐ [docs.termix.site/cli](https://docs.termix.site/cli). + +### ะ ะฐะทะผะตั‰ะตะฝะธะต ะฒ ะพะฑะปะฐะบะต + +ะกะตั€ะฒะตั€ Termix ะผะพะถะฝะพ ะดะตั€ะถะฐั‚ัŒ ะฝะฐ VPS, ะฐ ะฝะต ะฒะฝัƒั‚ั€ะธ ัะฒะพะตะน ัะตั‚ะธ. ะ•ัะปะธ Termix ั€ะฐะฑะพั‚ะฐะตั‚ ะฒ ั‚ะพะน ะถะต ัะตั‚ะธ, ะบะพั‚ะพั€ะพะน ัƒะฟั€ะฐะฒะปัะตั‚, ะฟั€ะธ ัะฑะพะต ะพะฝ ัƒะฟะฐะดั‘ั‚ ะฒะผะตัั‚ะต ั ะฝะตะน, ะบะฐะบ ั€ะฐะท ั‚ะพะณะดะฐ, ะบะพะณะดะฐ ะฝัƒะถะตะฝ ะดะปั ะฟะพั‡ะธะฝะบะธ. ะกะฝะฐั€ัƒะถะธ ะพะฝ ะพัั‚ะฐั‘ั‚ัั ะดะพัั‚ัƒะฟะฝั‹ะผ, ะดะฐั‘ั‚ ะฟะพัั‚ะพัะฝะฝั‹ะน IP ะธ ะฟะพะทะฒะพะปัะตั‚ ะทะฐะนั‚ะธ ะพั‚ะบัƒะดะฐ ัƒะณะพะดะฝะพ ะฑะตะท VPN ะธ ะฟั€ะพะฑั€ะพัะฐ ะฟะพั€ั‚ะพะฒ. + +[GINERNET](https://docs.termix.site/install/ginernet) ัะฟะพะฝัะธั€ัƒะตั‚ Termix, ะธ ะฒ ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ ะตัั‚ัŒ ะฟะพัˆะฐะณะพะฒะพะต ั€ัƒะบะพะฒะพะดัั‚ะฒะพ ะฟะพ ั€ะฐะทะฒั‘ั€ั‚ั‹ะฒะฐะฝะธัŽ ะฝะฐ ะธั… ะฟะปะพั‰ะฐะดะบะต VPS. +
-## ะŸะพะถะตั€ั‚ะฒะพะฒะฐะฝะธะต +## ะขะตะปะตะผะตั‚ั€ะธั -Termix ะฑะตัะฟะปะฐั‚ะตะฝ ะธ ะธะผะตะตั‚ ะพั‚ะบั€ั‹ั‚ั‹ะน ะธัั…ะพะดะฝั‹ะน ะบะพะด, ะฑะตะท ะฟะพะดะฟะธัะพะบ ะธะปะธ ะฟะปะฐั‚ะฝั‹ั… ั‚ะฐั€ะธั„ะพะฒ. ะ•ัะปะธ ะพะฝ ะฒะฐะผ ะฟะพะปะตะทะตะฝ, ั€ะฐััะผะพั‚ั€ะธั‚ะต ะฒะพะทะผะพะถะฝะพัั‚ัŒ ะฟะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั, ั‡ั‚ะพะฑั‹ ะฟะพะผะพั‡ัŒ ะฟะพะบั€ั‹ั‚ัŒ ั€ะฐัั…ะพะดั‹ ะฝะฐ ัะตั€ะฒะตั€ั‹, ะดะพะผะตะฝั‹ ะธ ะฒั€ะตะผั ั€ะฐะทั€ะฐะฑะพั‚ะบะธ. ะŸะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั ั‚ะฐะบะถะต ะฟะพะผะพะณะฐัŽั‚ ั„ะธะฝะฐะฝัะธั€ะพะฒะฐั‚ัŒ ะฒั€ะตะผั ะฝะฐ ะธััะปะตะดะพะฒะฐะฝะธะต ะธ ะธะทัƒั‡ะตะฝะธะต ั‚ะพะณะพ, ั‡ั‚ะพ ะฝะตะพะฑั…ะพะดะธะผะพ ะดะปั ัะพะทะดะฐะฝะธั ั‚ะฐะบะธั… ั„ัƒะฝะบั†ะธะน, ะบะฐะบ ะฟะพะดะดะตั€ะถะบะฐ SAML, Kubernetes ะธ Agent. ะžั‚ัะปะตะถะธะฒะฐะนั‚ะต ะฟั€ะพะณั€ะตัั ะธ ะดะตะปะฐะนั‚ะต ะฟะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั ะฝะธะถะต. +Termix ั€ะฐะท ะฒ ััƒั‚ะบะธ ะพั‚ะฟั€ะฐะฒะปัะตั‚ ะฝะตะฑะพะปัŒัˆะพะน ะฐะฝะพะฝะธะผะฝั‹ะน ัะธะณะฝะฐะป, ั‡ั‚ะพะฑั‹ ั ะฟะพะฝะธะผะฐะป, ัะบะพะปัŒะบะพ ัƒัั‚ะฐะฝะพะฒะพะบ ั€ะฐะฑะพั‚ะฐะตั‚ ะธ ะบะฐะบะธะผะธ ะฒะพะทะผะพะถะฝะพัั‚ัะผะธ ะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝะพ ะฟะพะปัŒะทัƒัŽั‚ัั. ะ’ ะฝั‘ะผ ะตัั‚ัŒ ัะปัƒั‡ะฐะนะฝั‹ะน ะธะดะตะฝั‚ะธั„ะธะบะฐั‚ะพั€ ัƒัั‚ะฐะฝะพะฒะบะธ, ั‡ะธัะปะพ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน ะธ ั…ะพัั‚ะพะฒ, ะฒะตั€ัะธั ะฟั€ะธะปะพะถะตะฝะธั ะธ ั‚ะพ, ะบะฐะบะธะต ะฒะพะทะผะพะถะฝะพัั‚ะธ (ั‚ะตั€ะผะธะฝะฐะป, ั„ะฐะนะปะพะฒั‹ะน ะผะตะฝะตะดะถะตั€, ั‚ัƒะฝะฝะตะปะธ, docker ะธ ะฟั€ะพั‡ะตะต) ะธัะฟะพะปัŒะทะพะฒะฐะปะธััŒ ะทะฐ ะฟะพัะปะตะดะฝะธะต 24 ั‡ะฐัะฐ. ะ’ ะฝั‘ะผ ะฝะธะบะพะณะดะฐ ะฝะตั‚ ะธะผั‘ะฝ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน, ะธะผั‘ะฝ ั…ะพัั‚ะพะฒ, IP-ะฐะดั€ะตัะพะฒ, ัƒั‡ั‘ั‚ะฝั‹ั… ะดะฐะฝะฝั‹ั… ะธ ะฝะธั‡ะตะณะพ ะดั€ัƒะณะพะณะพ, ั‡ั‚ะพ ัƒะบะฐะทั‹ะฒะฐะปะพ ะฑั‹ ะฝะฐ ะฒะฐั ะธะปะธ ะฒะฐัˆะธ ัะตั€ะฒะตั€ั‹. -[ะŸะพะถะตั€ั‚ะฒะพะฒะฐั‚ัŒ](https://donate.termix.site/) +ะŸะพ ัƒะผะพะปั‡ะฐะฝะธัŽ ะพะฝ ะฒะบะปัŽั‡ั‘ะฝ. ะ’ั‹ะบะปัŽั‡ะธั‚ัŒ ะผะพะถะฝะพ ะฒ ะฝะฐัั‚ั€ะพะนะบะฐั… ะฐะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ะฐ ะฒ ั€ะฐะทะดะตะปะต ยซะžะฑั‰ะธะตยป ะธะปะธ ะทะฐะดะฐั‚ัŒ `ENABLE_TELEMETRY=false` ะตั‰ั‘ ะดะพ ะฟะตั€ะฒะพะณะพ ะทะฐะฟัƒัะบะฐ Termix. + +
+ +## ะŸะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั + +Termix ะฑะตัะฟะปะฐั‚ะตะฝ ะธ ะพั‚ะบั€ั‹ั‚, ะฑะตะท ะฟะพะดะฟะธัะพะบ ะธ ะฟะปะฐั‚ะฝั‹ั… ั‚ะฐั€ะธั„ะพะฒ. ะ•ัะปะธ ะพะฝ ะฒะฐะผ ะฟะพะปะตะทะตะฝ, ะฟะพะดัƒะผะฐะนั‚ะต ะพ ะฟะพะถะตั€ั‚ะฒะพะฒะฐะฝะธะธ: ะพะฝะพ ะฟะพะผะพะณะฐะตั‚ ั ัะตั€ะฒะตั€ะฐะผะธ, ะดะพะผะตะฝะฐะผะธ ะธ ะฒั€ะตะผะตะฝะตะผ ะฝะฐ ั€ะฐะทั€ะฐะฑะพั‚ะบัƒ. ะŸะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั ั‚ะฐะบะถะต ะพะฟะปะฐั‡ะธะฒะฐัŽั‚ ะฒั€ะตะผั ะฝะฐ ะธะทัƒั‡ะตะฝะธะต ั‚ะพะณะพ, ั‡ั‚ะพ ะฝัƒะถะฝะพ ะดะปั ั‚ะฐะบะธั… ะฒะพะทะผะพะถะฝะพัั‚ะตะน, ะบะฐะบ SAML, Kubernetes ะธ ะฟะพะดะดะตั€ะถะบะฐ ะฐะณะตะฝั‚ะพะฒ. ะกะปะตะดะธั‚ัŒ ะทะฐ ั…ะพะดะพะผ ั€ะฐะฑะพั‚ ะธ ะฟะพะดะดะตั€ะถะฐั‚ัŒ ะผะพะถะฝะพ ะฟะพ ััั‹ะปะบะต ะฝะธะถะต. + +[ะŸะพะดะดะตั€ะถะฐั‚ัŒ](https://donate.termix.site/)
## ะกะฟะพะฝัะพั€ั‹ -ะ—ะฐะธะฝั‚ะตั€ะตัะพะฒะฐะฝั‹ ะฒ ะฟะปะฐั‚ะฝะพะผ ั€ะฐะทะผะตั‰ะตะฝะธะธ ะดะปั ะฟะพะดะดะตั€ะถะบะธ ั€ะฐะทั€ะฐะฑะพั‚ะบะธ? ะะฐะฟะธัˆะธั‚ะต ะฝะฐ [mail@termix.site](mailto:mail@termix.site). +ะ˜ะฝั‚ะตั€ะตััƒะตั‚ ะฟะปะฐั‚ะฝะพะต ั€ะฐะทะผะตั‰ะตะฝะธะต ะฒ ะฟะพะดะดะตั€ะถะบัƒ ั€ะฐะทั€ะฐะฑะพั‚ะบะธ? ะะฐะฟะธัˆะธั‚ะต ะฝะฐ [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Termix ะฑะตัะฟะปะฐั‚ะตะฝ ะธ ะธะผะตะตั‚ ะพั‚ะบั€ั‹ั‚ั‹ะน ะธัั…ะพะดะฝั‹ะน ะบะพะด Cloudflare     - - Tailscale - -    Akamai @@ -340,14 +421,17 @@ Termix ะฑะตัะฟะปะฐั‚ะตะฝ ะธ ะธะผะตะตั‚ ะพั‚ะบั€ั‹ั‚ั‹ะน ะธัั…ะพะดะฝั‹ะน ะบะพะด Rack Genius - +    + + Ginernet +

## ะŸะพะดะดะตั€ะถะบะฐ -ะ•ัะปะธ ะฒะฐะผ ะฝัƒะถะฝะฐ ะฟะพะผะพั‰ัŒ ะธะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ะทะฐะฟั€ะพัะธั‚ัŒ ะฝะพะฒัƒัŽ ั„ัƒะฝะบั†ะธัŽ ะดะปั Termix, ะฟะพัะตั‚ะธั‚ะต ัั‚ั€ะฐะฝะธั†ัƒ [ะŸั€ะพะฑะปะตะผั‹](https://github.com/Termix-SSH/Support/issues), ะฒะพะนะดะธั‚ะต ะฒ ัะธัั‚ะตะผัƒ ะธ ะฝะฐะถะผะธั‚ะต `New Issue`. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะพะฟะธัˆะธั‚ะต ะฒะฐัˆัƒ ะฟั€ะพะฑะปะตะผัƒ ะบะฐะบ ะผะพะถะฝะพ ะฟะพะดั€ะพะฑะฝะตะต, ะฟั€ะตะดะฟะพั‡ั‚ะธั‚ะตะปัŒะฝะพ ะฝะฐ ะฐะฝะณะปะธะนัะบะพะผ ัะทั‹ะบะต. ะ’ั‹ ั‚ะฐะบะถะต ะผะพะถะตั‚ะต ะฟั€ะธัะพะตะดะธะฝะธั‚ัŒัั ะบ ัะตั€ะฒะตั€ัƒ [Discord](https://discord.gg/jVQGdvHDrf) ะธ ะพะฑั€ะฐั‚ะธั‚ัŒัั ะฒ ะบะฐะฝะฐะป ะฟะพะดะดะตั€ะถะบะธ, ะพะดะฝะฐะบะพ ะฒั€ะตะผั ะพั‚ะฒะตั‚ะฐ ะผะพะถะตั‚ ะฑั‹ั‚ัŒ ะดะพะปัŒัˆะต. +ะัƒะถะฝะฐ ะฟะพะผะพั‰ัŒ ะธะปะธ ั…ะพั‚ะธั‚ะต ะฟั€ะตะดะปะพะถะธั‚ัŒ ั„ัƒะฝะบั†ะธัŽ? ะกะพะทะดะฐะนั‚ะต [ะฝะพะฒะพะต ะพะฑั€ะฐั‰ะตะฝะธะต](https://github.com/Termix-SSH/Support/issues) ะธ ะพะฟะธัˆะธั‚ะต ะฒัั‘ ะบะฐะบ ะผะพะถะฝะพ ะฟะพะดั€ะพะฑะฝะตะต, ะฟะพ ะฒะพะทะผะพะถะฝะพัั‚ะธ ะฝะฐ ะฐะฝะณะปะธะนัะบะพะผ. ะ•ั‰ั‘ ะผะพะถะฝะพ ัะฟั€ะพัะธั‚ัŒ ะฒ ะบะฐะฝะฐะปะต ะฟะพะดะดะตั€ะถะบะธ ะฒ [Discord](https://discord.gg/jVQGdvHDrf), ั…ะพั‚ั ั‚ะฐะผ ะพั‚ะฒะตั‚ะฐ ะธะฝะพะณะดะฐ ะฟั€ะธั…ะพะดะธั‚ัั ะถะดะฐั‚ัŒ ะดะพะปัŒัˆะต.
@@ -399,7 +483,7 @@ Termix ะฑะตัะฟะปะฐั‚ะตะฝ ะธ ะธะผะตะตั‚ ะพั‚ะบั€ั‹ั‚ั‹ะน ะธัั…ะพะดะฝั‹ะน ะบะพะด
ะŸะปะฐั‚ั„ะพั€ะผะฐะ”ะธัั‚ั€ะธะฑัƒั‚ะธะฒะกะฟะพัะพะฑ ัƒัั‚ะฐะฝะพะฒะบะธ
Web
-ะะตะบะพั‚ะพั€ั‹ะต ะฒะธะดะตะพ ะธ ะธะทะพะฑั€ะฐะถะตะฝะธั ะผะพะณัƒั‚ ะฑั‹ั‚ัŒ ัƒัั‚ะฐั€ะตะฒัˆะธะผะธ ะธะปะธ ะฝะต ะฟะพะปะฝะพัั‚ัŒัŽ ะพั‚ั€ะฐะถะฐั‚ัŒ ั„ัƒะฝะบั†ะธะพะฝะฐะปัŒะฝะพัั‚ัŒ. +ะะตะบะพั‚ะพั€ั‹ะต ะฒะธะดะตะพ ะธ ะธะทะพะฑั€ะฐะถะตะฝะธั ะผะพะณัƒั‚ ัƒัั‚ะฐั€ะตั‚ัŒ ะธะปะธ ะฝะต ะฟะพะปะฝะพัั‚ัŒัŽ ะฟะพะบะฐะทั‹ะฒะฐั‚ัŒ ะฒะพะทะผะพะถะฝะพัั‚ะธ. @@ -407,10 +491,10 @@ Termix ะฑะตัะฟะปะฐั‚ะตะฝ ะธ ะธะผะตะตั‚ ะพั‚ะบั€ั‹ั‚ั‹ะน ะธัั…ะพะดะฝั‹ะน ะบะพะด ## ะ—ะฐะฟะปะฐะฝะธั€ะพะฒะฐะฝะฝั‹ะต ั„ัƒะฝะบั†ะธะธ -ะกะผะพั‚ั€ะธั‚ะต [ะŸั€ะพะตะบั‚ั‹](https://github.com/orgs/Termix-SSH/projects/5) ะดะปั ะฟั€ะพัะผะพั‚ั€ะฐ ะฒัะตั… ะทะฐะฟะปะฐะฝะธั€ะพะฒะฐะฝะฝั‹ั… ั„ัƒะฝะบั†ะธะน. ะ•ัะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ะฒะฝะตัั‚ะธ ะฒะบะปะฐะด, ัะผะพั‚ั€ะธั‚ะต [ะฃั‡ะฐัั‚ะธะต ะฒ ั€ะฐะทั€ะฐะฑะพั‚ะบะต](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +ะ’ัะต ะทะฐะฟะปะฐะฝะธั€ะพะฒะฐะฝะฝั‹ะต ั„ัƒะฝะบั†ะธะธ ัะพะฑั€ะฐะฝั‹ ะฒ [Projects](https://github.com/orgs/Termix-SSH/projects/5). ะ•ัะปะธ ั…ะพั‚ะธั‚ะต ะฟะพัƒั‡ะฐัั‚ะฒะพะฒะฐั‚ัŒ, ะฟะพัะผะพั‚ั€ะธั‚ะต [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
## ะ›ะธั†ะตะฝะทะธั -ะ ะฐัะฟั€ะพัั‚ั€ะฐะฝัะตั‚ัั ะฟะพ ะปะธั†ะตะฝะทะธะธ Apache License Version 2.0. ะŸะพะดั€ะพะฑะฝะตะต ัะผ. ะฒ ั„ะฐะนะปะต `LICENSE`. +ะ ะฐัะฟั€ะพัั‚ั€ะฐะฝัะตั‚ัั ะฟะพ ะปะธั†ะตะฝะทะธะธ Apache ะฒะตั€ัะธะธ 2.0. ะŸะพะดั€ะพะฑะฝะพัั‚ะธ ะฒ ั„ะฐะนะปะต `LICENSE`. diff --git a/docs/readme/README-TR.md b/docs/readme/README-TR.md index 1ee719b..bb2111e 100644 --- a/docs/readme/README-TR.md +++ b/docs/readme/README-TR.md @@ -4,7 +4,7 @@

Termix

-

Kendi sunucunuzda barindirilan SSH yonetimi ve uzak masaustu erisimi

+

Kendi sunucunuzda รงalฤฑลŸan sunucu yรถnetimi, SSH ve uzak masaรผstรผnden otomasyonlara kadar

English ยท @@ -37,7 +37,7 @@
-Termix รผcretsiz ve aรงฤฑk kaynaklฤฑdฤฑr. Faydalฤฑ buluyorsanฤฑz, sunucu maliyetleri ve geliลŸtirme sรผresine katkฤฑda bulunmak iรงin [baฤŸฤฑลŸ yapmayฤฑ](https://donate.termix.site/) dรผลŸรผnebilirsiniz. +Termix รผcretsiz ve aรงฤฑk kaynaklฤฑdฤฑr. ฤฐลŸinize yarฤฑyorsa, sunucu masraflarฤฑna ve geliลŸtirme sรผresine katkฤฑ iรงin [baฤŸฤฑลŸ yapmayฤฑ](https://donate.termix.site/) dรผลŸรผnรผn.
@@ -49,145 +49,201 @@ Termix รผcretsiz ve aรงฤฑk kaynaklฤฑdฤฑr. Faydalฤฑ buluyorsanฤฑz, sunucu maliyet

Repo of the Day Achievement
- 1 Eylรผl 2025'te kazanildi + 1 Eylรผl 2025 tarihinde kazanฤฑldฤฑ


-## Genel Bakis +## Genel bakฤฑลŸ -Termix, acik kaynakli, sonsuza kadar ucretsiz, kendi sunucunuzda barindirabileceginez hepsi bir arada sunucu yonetim platformudur. Sunucularinizi ve altyapinizi tek bir sezgisel arayuz uzerinden yonetmek icin cok platformlu bir cozum sunar. Termix, SSH terminal erisimi, uzak masaustu kontrolu (RDP, VNC, Telnet), SSH tunelleme yetenekleri, uzak dosya yonetimi ve daha bircok arac saglar. Termix, tum platformlarda kullanilabilen Termius'un mukemmel ucretsiz ve kendi barindirmali alternatifidir. +Termix, sunucularฤฑnฤฑzฤฑ yรถnetmek iรงin รผcretsiz, aรงฤฑk kaynaklฤฑ ve kendi sunucunuzda รงalฤฑลŸan bir platformdur. SSH terminallerini, uzak masaรผstlerini (RDP, VNC, Telnet), dosya aktarฤฑmlarฤฑnฤฑ, tรผnelleri, Docker'ฤฑ, รถlรงรผmleri ve otomasyonlarฤฑ tek yerde toplar; web, masaรผstรผ ve mobilde รงalฤฑลŸฤฑr. Sonsuza dek รผcretsiz kalan, kendi sunucunuzda รงalฤฑลŸan bir Termius alternatifidir.
-## Ozellikler +## ร–zellikler + + + + + + + + + + + + + + + + @@ -196,43 +252,46 @@ Yaklasik 30 dil icin yerlesik destek ([Crowdin](https://docs.termix.site/transla
-Daha fazla ozellik +Daha fazla รถzellik
-- **Kontrol Paneli** - Kontrol panelinizde sunucu bilgilerini bir bakista goruntuleyin -- **API Anahtarlari** - Otomasyon/CI icin kullanilmak uzere son kullanma tarihleriyle kullanici kapsamli API anahtarlari olusturun -- **Veri Disa/Ice Aktarma** - SSH ana bilgisayarlarini, kimlik bilgilerini ve dosya yoneticisi verilerini disa ve ice aktarin -- **Otomatik SSL Kurulumu** - HTTPS yonlendirmeleriyle yerlesik SSL sertifika olusturma ve yonetimi -- **Modern Arayuz** - React, Tailwind CSS ve Shadcn ile olusturulmus temiz masaustu/mobil uyumlu arayuz. Isik, karanlik, Dracula vb. dahil olmak uzere bircok farkli UI temasi arasฤฑndan secim yapin. Herhangi bir baglantฤฑyฤฑ tam ekranda acmak icin URL yollarini kullanin. -- **Komut Gecmisi** - Daha once calistirilan SSH komutlarini otomatik tamamlayin ve goruntuleyin -- **Hizli Baglanti** - Baglanti verilerini kaydetmeden bir sunucuya baglanin -- **Komut Paleti** - Sol shift tusuna iki kez basarak SSH baglantilariniza klavyenizle hizlica erisin -- **Proxmox Entegrasyonu** - Proxmox ornekinizden Termix'e otomatik olarak ana bilgisayar ekleyin -- **SSH Zengin Ozellikler** - Atlama ana bilgisayarlari, Warpgate, TOTP tabanli baglantilar, SOCKS5, ana bilgisayar anahtar dogrulama, otomatik sifre doldurma, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, terminal gunlukleme, SSH agent forwarding, Bitwarden SSH agent, HashiCorp Vault SSH imzalama ve dahasini destekler. -- **Termix ID** - Termix'e entegre edilmis bir sshid.io esdegeri. Bir kullanici adi edinin, genel SSH anahtarlarinizi bir cozumleyici URL'sinde yayinlayin ve SSH sertifikalari vermek icin yerlesik bir CA kullanin. +- **Kontrol paneli** - Kendi dizdiฤŸiniz kartlarla sunucularฤฑnฤฑza tek bakฤฑลŸta gรถz atฤฑn +- **AฤŸ grafiฤŸi** - Ev laboratuvarฤฑnฤฑz sunucularฤฑnฤฑzdan รงizilir, durum anlฤฑk gรถsterilir +- **Tmux izleyici** - tmux oturumlarฤฑna, pencerelerine ve panellerine รถnizleme ve aramayla gรถz atฤฑn +- **API anahtarlarฤฑ** - Betikler ve CI iรงin, son kullanma tarihli kullanฤฑcฤฑya รถzel anahtarlar +- **DฤฑลŸa ve iรงe aktarma** - Sunucularฤฑ, kimlik bilgilerini ve dosya yรถneticisi verilerini taลŸฤฑyฤฑn +- **Otomatik SSL** - Sertifikalar sizin iรงin oluลŸturulur ve yenilenir, HTTPS yรถnlendirmesiyle birlikte; ya da kendi sertifikanฤฑzฤฑ kullanฤฑn +- **Veritabanlarฤฑ** - Varsayฤฑlan SQLite, ayrฤฑca PostgreSQL ve MySQL desteklenir +- **Modern arayรผz** - Masaรผstรผ ve mobilde รงalฤฑลŸan sade bir React arayรผzรผ; aรงฤฑk, koyu ve Dracula gibi temalarla. Her baฤŸlantฤฑ bir adresten tam ekran aรงฤฑlabilir +- **Komut paleti** - Sol Shift'e iki kez basarak klavyeden bir sunucuya atlayฤฑn +- **Klavye kฤฑsayollarฤฑ** - Sekmeler arasฤฑnda geรงiลŸ, sekme kapatma ve dahasฤฑ, hepsi yeniden atanabilir +- **Wake-on-LAN** - Bir makineyi Termix'ten ya da bir otomasyon adฤฑmฤฑndan uyandฤฑrฤฑn +- **Gรผvenilir vekil doฤŸrulamasฤฑ** - GiriลŸi ters vekil sunucu halletsin ve kullanฤฑcฤฑyฤฑ aktarsฤฑn +- **Zengin SSH desteฤŸi** - Atlama sunucularฤฑ, Warpgate, TOTP istekleri, SOCKS5, sunucu anahtarฤฑ doฤŸrulama, parola otomatik doldurma, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, terminal gรผnlรผฤŸรผ, aracฤฑ yรถnlendirme, Bitwarden SSH aracฤฑsฤฑ, HashiCorp Vault ile SSH imzalama ve dahasฤฑ +- **Termix ID** - sshid.io'nun yerleลŸik hali. Bir kullanฤฑcฤฑ adฤฑ alฤฑn, aรงฤฑk anahtarlarฤฑnฤฑzฤฑ bir รงรถzรผmleyici adresinde yayฤฑmlayฤฑn ve yerleลŸik CA ile SSH sertifikalarฤฑ รงฤฑkarฤฑn

-## Platform Destegi +## Platform desteฤŸi
-**SSH Terminal Erisimi:** -Tarayici benzeri sekme sistemiyle bolunmus ekran destegine sahip (4 panele kadar) tam ozellikli terminal. Yaygin terminal temalari, yazi tipleri ve diger bilesenleri iceren terminal ozellestirme destegi. +**SSH terminali:** +Tarayฤฑcฤฑ gibi sekmeleri ve bรถlรผnmรผลŸ ekranฤฑ olan tam donanฤฑmlฤฑ bir terminal, aynฤฑ anda 6 panele kadar. Temanฤฑzฤฑ, yazฤฑ tipinizi ve renklerinizi seรงin. Her oturumun รผstรผnde anlฤฑk CPU, bellek ve disk bilgisi gรถsteren bir araรง รงubuฤŸu ile o sunucunun dosyalarฤฑna, Docker'ฤฑna, tรผnellerine ve รถlรงรผmlerine giden kฤฑsayollar bulunur. -**Uzak Masaustu Erisimi:** -Tam ozellestirme ve bolunmus ekran ile tarayici uzerinden RDP, VNC ve Telnet destegi. +**Uzak masaรผstรผ:** +Tarayฤฑcฤฑda RDP, VNC ve Telnet; diฤŸer oturumlar gibi sekmelerde ve bรถlรผnmรผลŸ ekranda. RDP sรผrรผcรผleri iรงin dosya tarayฤฑcฤฑsฤฑ ve sรผrรผkle bฤฑrak yรผkleme iรงerir. Windows masaรผstรผnde bir sunucuyu yerel RDP istemcisinde de aรงabilirsiniz.
-**SSH Tunel Yonetimi:** -Otomatik yeniden baglanti, saglik izleme ve yerel, uzak veya dinamik SOCKS yonlendirme ile sunucular arasi SSH tunelleri olusturun ve yonetin. Masaustu istemci-sunucu tunel ayarlari her masaustu kurulumu icin yerel olarak depolanir; istege bagli C2S hazir ayar anlik goruntuleri, yerel bir tunel yapilandirmasini istemciler arasinda tasimak istediginizde sunucuya kaydedilebilir, yeniden adlandirilabilir, yuklenebilir veya silinebilir. +**SSH tรผnelleri:** +Yerel, uzak ve dinamik SOCKS yรถnlendirmesi; otomatik yeniden baฤŸlanma ve durum kontrolleriyle. Masaรผstรผ uygulamasฤฑndaki istemciden sunucuya tรผneller o makinede saklanฤฑr, ayarlarฤฑ sunucuya kaydederek bir kurulumu baลŸka bir makineye taลŸฤฑyabilirsiniz. -**Uzak Dosya Yoneticisi:** -Uzak sunuculardaki dosyalari dogrudan yonetin; kod, goruntu, ses ve video goruntuleme ve duzenleme destegi ile. Sudo destegi ile dosyalari sorunsuzca yukleyin, indirin, yeniden adlandirin, silin ve tasiyin. Dosyalari sunucudan sunucuya tasima destegini de icerir. +**Dosya yรถneticisi:** +SFTP รผzerinden dosyalara gรถz atฤฑn, dรผzenleyin, yรผkleyin, indirin, yeniden adlandฤฑrฤฑn, taลŸฤฑyฤฑn ve silin; sudo da kullanฤฑlabilir. Kod, gรถrsel, ses ve videoyu gรถrรผntรผleyip dรผzenleyin. Dosyalarฤฑ doฤŸrudan bir sunucudan diฤŸerine kopyalayฤฑn; en hฤฑzlฤฑ yol sizin iรงin seรงilir ve aktarฤฑmlarฤฑn bรผtรผnlรผฤŸรผ doฤŸrulanฤฑr.
-**Docker ve Podman Yonetimi:** -Konteynerleri baslatฤฑn, durdurun, duraklatฤฑn, kaldirin. Konteyner istatistiklerini goruntuleyin. Docker exec terminali kullanarak konteyneri kontrol edin. Docker ve Podman'i konteyner calisma ortami olarak destekler. Portainer veya Dockge'nin yerini almak icin degil, konteynerlerinizi olusturmak yerine basitce yonetmek icin tasarlanmistir. +**Docker ve Podman:** +Kapsayฤฑcฤฑlarฤฑ baลŸlatฤฑn, durdurun, duraklatฤฑn ve silin, durumlarฤฑnฤฑ izleyin ve iรงlerinde bir kabuk aรงฤฑn. Hem Docker hem Podman ile รงalฤฑลŸฤฑr. Portainer ya da Dockge'nin yerini almak iรงin deฤŸil, hรขlihazฤฑrdaki kapsayฤฑcฤฑlarฤฑnฤฑzฤฑ yรถnetmek iรงin tasarlandฤฑ. -**SSH Ana Bilgisayar Yoneticisi:** -SSH baglantilarinizi etiketler ve klasorlerle (klasor ozellestirme ve ic ice klasor destegi ile) kaydedin, duzenleyin ve yonetin; yeniden kullanilabilir giris bilgilerini kolayca kaydedin ve SSH anahtarlarinin dagitimini otomatiklestirin. +**Sunucu yรถneticisi:** +Sunucularฤฑnฤฑzฤฑ etiketlerle ve isim ve renk verebileceฤŸiniz iรง iรงe klasรถrlerle dรผzenleyin. Kayฤฑtlฤฑ kimlik bilgilerini birden รงok sunucuda kullanฤฑn, SSH anahtarlarฤฑnฤฑ otomatik daฤŸฤฑtฤฑn, sunucularฤฑ bir รผst sunucunun altฤฑnda toplayฤฑn, toplu dรผzenleyip dฤฑลŸa aktarฤฑn ve kaydetmek istemediฤŸiniz tek seferlik baฤŸlantฤฑlar iรงin hฤฑzlฤฑ baฤŸlantฤฑyฤฑ kullanฤฑn.
-**Ana Bilgisayar Metrikleri:** -Cogu Linux tabanli sunucularda calisan CPU, bellek, disk kullanimi, ag, calisma suresi, sistem bilgisi, guvenlik duvari, port izleme, gunluk goruntuleyici, kullanicilar/izinler, sertifikalar ve daha fazlasini goruntuleyin. Zaman serisi gecmis grafiklerini ve ntfy ile webhook destekli esik tabanli uyarilari icerir. +**Sunucu รถlรงรผmleri:** +ร‡oฤŸu Linux sunucusunda CPU, bellek, disk, aฤŸ, sฤฑcaklฤฑk, รงalฤฑลŸma sรผresi, sรผreรงler, portlar, oturum aรงmalar ve sistem bilgisi; geรงmiลŸ grafikleriyle birlikte. Yรถnetim kartlarฤฑ sayesinde servisleri, cron gรถrevlerini, paketleri, kullanฤฑcฤฑlarฤฑ, gรผvenlik duvarฤฑ kurallarฤฑnฤฑ, WireGuard'ฤฑ, Tailscale'i, SSL sertifikalarฤฑnฤฑ, gรผnlรผkleri ve saฤŸlฤฑk kontrollerini Termix'ten รงฤฑkmadan yรถnetirsiniz. -**Kullanici Kimlik Dogrulama:** -Yonetici kontrolleri (diger kullanicilarin bilgilerini duzenleyebilir), OIDC/LDAP/SSO (erisim kontrollu), 2FA (TOTP) ve passkey (WebAuthn) destegi ile guvenli kullanici yonetimi. Tum platformlardaki aktif kullanici oturumlarini goruntuleyin ve izinleri iptal edin. OIDC/Yerel hesaplarinizi birbirine baglayin. Tum kullanicilarin islemlerinin denetim gunlugunu goruntuleyin. +**Otomasyonlar:** +Bir tetikleyici seรงin, sonra ne olacaฤŸฤฑnฤฑ sรถyleyin. Tetikleyiciler arasฤฑnda bir รถlรงรผmรผn eลŸiฤŸi aลŸmasฤฑ, bir sunucunun dรผลŸmesi veya geri gelmesi, saฤŸlฤฑk kontrolรผnรผn deฤŸiลŸmesi, bir zamanlama, bir kapsayฤฑcฤฑ olayฤฑ ya da gelen bir webhook var. Adฤฑmlar komut ve parรงacฤฑk รงalฤฑลŸtฤฑrabilir, kapsayฤฑcฤฑ ve tรผnelleri yรถnetebilir, bir makineyi uyandฤฑrabilir, bir adrese istek atabilir, bekleyebilir, koลŸula gรถre dallanabilir, baลŸka bir otomasyonu รงalฤฑลŸtฤฑrabilir ve ntfy, Discord ya da webhook ile size haber verebilir. Deneme รงalฤฑลŸtฤฑrmalarฤฑ ile รถnce gรผvenle test edersiniz.
-**Tailscale Entegrasyonu:** -Tailscale aginizdaki cihazlari listeleyerek hizlica ana bilgisayar olarak ekleyin ve kimlik dogrulama yontemi olarak Tailscale SSH kullanarak baglanin; bu sayede ag ACL'leriniz kimlik bilgileri depolamadan yetkilendirmeyi yonetir. +**Filolar:** +Sunucularฤฑ tek tek seรงerek ya da etiket kurallarฤฑyla bir filoda toplayฤฑn; yeni sunucular kendiliฤŸinden katฤฑlsฤฑn. Tek bir komutu tรผm sunucularda aynฤฑ anda รงalฤฑลŸtฤฑrฤฑn, hepsine dosya gรถnderip hepsinden dosya alฤฑn, paket kurun ve iลŸletim sistemi, รงekirdek, mimari ve รงalฤฑลŸma sรผresi dรถkรผmรผnรผ toplayฤฑn. -**RBAC/Paylasim:** -Roller olusturun ve ana bilgisayarlari kullanicilar/roller arasinda paylasin. Tum kimlik dogrulama turlerini ve tum ana bilgisayar protokollerini destekler. +**Yapay zekรข asistanฤฑ:** +ฤฐsteฤŸe baฤŸlฤฑdฤฑr ve siz aรงana kadar kapalฤฑdฤฑr. OpenAI, Anthropic, Gemini, Ollama ya da OpenAI uyumlu herhangi bir uรง noktayฤฑ baฤŸlayฤฑn ve kurulumunuz hakkฤฑnda sorular sorun. Sunucularฤฑ, filolarฤฑ, parรงacฤฑklarฤฑ ve uyarฤฑlarฤฑ okuyabilir; deฤŸiลŸiklikleri kendisi yapmak yerine onayฤฑnฤฑza sunar. Kimlik bilgilerine, kullanฤฑcฤฑlara ve ayarlara asla eriลŸemez. Yรถneticiler tรผm kurulum iรงin kapalฤฑ bฤฑrakabilir, siz de kurulum sฤฑrasฤฑnda gizleyebilirsiniz.
-**Seri Baglantilar:** -Seri cihazlara (router, switch, mikrodenetleyici vb.) dogrudan tarayici veya masaustu uygulamasindan baglanin. Baud hizi, veri bitleri, durdurma bitleri ve parite yapilandirin. Desteklenen tarayicilarda Web Serial API, Electron uygulamasinda yerel arka ucu kullanir. +**GiriลŸ ve kullanฤฑcฤฑlar:** +Yerel hesaplarฤฑn yanฤฑnda OIDC, LDAP, GitHub ve Google ile giriลŸ; iki adฤฑmlฤฑ doฤŸrulama (TOTP), geรงiลŸ anahtarlarฤฑ (WebAuthn) ve gรผvenilir cihazlar. Yรถneticiler kullanฤฑcฤฑlarฤฑ yรถnetebilir, OIDC gruplarฤฑnฤฑ rollerle eลŸleลŸtirebilir, tรผm platformlardaki etkin oturumlarฤฑ gรถrรผp sonlandฤฑrabilir. Yerel ve OIDC hesaplarฤฑnฤฑzฤฑ birbirine baฤŸlayฤฑn ve herkesin ne yaptฤฑฤŸฤฑnฤฑ denetim gรผnlรผฤŸรผnden okuyun. -**Uyarilar:** -Ana bilgisayar metrikleri (CPU, bellek, disk vb.) icin esik tabanli uyari kurallari belirleyin ve tetiklendiklerinde ntfy veya webhook araciligiyla bildirim alin. Gecmis gunlugunde tetiklenen ve cozulen uyarilari goruntuleyin. +**Roller ve paylaลŸฤฑm:** +Roller oluลŸturun ve sunucularฤฑ kullanฤฑcฤฑlar veya rollerle dรถrt dรผzeyde paylaลŸฤฑn: baฤŸlanma, gรถrรผntรผleme, dรผzenleme ve yรถnetme. Tรผm kimlik doฤŸrulama tรผrleri ve tรผm protokollerle รงalฤฑลŸฤฑr, paylaลŸฤฑlan bir sunucuda kullanฤฑlan kimlik bilgilerini deฤŸiลŸtirebilirsiniz.
-**Ana Sayfa:** -Surukleme ve birakma widget izgarasina sahip tamamen ozellestirilebilir bir ana sayfa. Ana bilgisayar durumu, hizmet baglantilari, saatler, notlar, RSS besleme, hava durumu, Docker konteynerleri, ana bilgisayar metrik grafikleri, gomulu terminaller, iframe ve daha fazlasi icin widget ekleyin. +**Uyarฤฑlar:** +CPU, bellek ve disk gibi sunucu รถlรงรผmlerine kurallar koyun ve tetiklendiklerinde ntfy, Discord veya webhook ile haberdar olun. Devam eden ve รงรถzรผlen uyarฤฑlarฤฑ geรงmiลŸte gรถrรผn, ilgilenmediklerinizi kapatฤฑn. -**Veritabani Sifreleme:** -Arka uc, sifrelenmis SQLite veritabani dosyalari olarak depolanir. Daha fazla bilgi icin [belgelere](https://docs.termix.site/security) bakin. +**Ana sayfa:** +Kendi kurduฤŸunuz, sรผrรผkle bฤฑrak รงalฤฑลŸan bir bileลŸen ฤฑzgarasฤฑ. Sunucu durumu, ping, servis baฤŸlantฤฑlarฤฑ, yer imleri, arama, saatler, takvimler, geri sayฤฑmlar, notlar, RSS, hava durumu, gรถrseller, gรถmรผlรผ sayfalar, Docker, tรผneller, รถlรงรผm grafikleri, kendi API'leriniz ve hatta canlฤฑ bir terminal iรงin bileลŸenler var.
-**Ag Grafigi:** -Kontrol panelinizi, SSH baglantilariniza dayali olarak ev laboratuvarinizi durum destegi ile gorselletirmek icin ozellestirin. +**Parรงacฤฑklar ve araรงlar:** +Sฤฑk kullandฤฑฤŸฤฑnฤฑz komutlarฤฑ kaydedin ve tek tฤฑkla รงalฤฑลŸtฤฑrฤฑn; sunucu iรงin ve kendi girdileriniz iรงin deฤŸiลŸkenler kullanabilirsiniz. Aynฤฑ komutu aรงฤฑk olan tรผm terminallerde รงalฤฑลŸtฤฑrฤฑn, komut geรงmiลŸinizde tamamlamayla arama yapฤฑn. -**SSH Araclari:** -Tek tiklamayla calistirilan yeniden kullanilabilir komut parcaciklari olusturun. Birden fazla acik terminalde ayni anda tek bir komut calistirin. +**Oturum paylaลŸฤฑmฤฑ:** +Canlฤฑ bir terminal, RDP, VNC veya Telnet oturumunu paylaลŸฤฑn. Hesap gerekmeden katฤฑlฤฑnabilen bir baฤŸlantฤฑ gรถnderin ya da belirli bir Termix kullanฤฑcฤฑsฤฑyla, salt okunur veya yazma yetkili olarak paylaลŸฤฑn. PaylaลŸฤฑmlar kendiliฤŸinden sona erebilir veya istediฤŸiniz an iptal edilebilir; tรผmรผyle ya da sunucu bazฤฑnda kapatฤฑlabilir.
-**Kalici Sekmeler:** -Kullanici profilinde etkinlestirilmisse SSH oturumlari ve sekmeler cihazlar/yenilemeler arasinda acik kalir. +**Oturum kaydฤฑ ve gรผnlรผkler:** +Terminal, RDP ve VNC oturumlarฤฑnฤฑ kaydedin ve sonra izleyin. Bir oturumun dรผz metin gรผnlรผฤŸรผnรผ indirin, baฤŸlantฤฑ gรผnlรผฤŸรผne bakarak baฤŸlantฤฑ sฤฑrasฤฑnda tam olarak ne olduฤŸunu gรถrรผn. + + + +**Seri baฤŸlantฤฑlar:** +Yรถnlendirici, anahtar ve mikrodenetleyici gibi seri cihazlarla tarayฤฑcฤฑdan veya masaรผstรผ uygulamasฤฑndan konuลŸun. Baud hฤฑzฤฑnฤฑ, veri bitlerini, dur bitlerini ve pariteyi ayarlayฤฑn. Destekleyen tarayฤฑcฤฑlarda Web Serial API'yi, masaรผstรผ uygulamasฤฑnda yerel bir arka ucu kullanฤฑr. + +
+ +**Tailscale:** +Tailnet'inizdeki cihazlarฤฑ รงekip birkaรง tฤฑkla sunucu olarak ekleyin ve Tailscale SSH ile baฤŸlanฤฑn; eriลŸimi tailnet kurallarฤฑnฤฑz yรถnetsin, kimlik bilgisi saklamanฤฑz gerekmesin. Headscale ve รถzel uรง noktalar da รงalฤฑลŸฤฑr. + + + +**Proxmox:** +Sunucularฤฑ doฤŸrudan bir Proxmox kurulumundan iรงe aktarฤฑn; dรผฤŸรผm ve misafir makinelerin CPU, bellek ve depolama dahil durumlarฤฑnฤฑ kendi sekmesinde izleyin. + +
+ +**ร‡alฤฑลŸma alanlarฤฑ ve sekmeler:** +Bir sekme grubunu bรถlรผnmรผลŸ dรผzeniyle birlikte kaydedin ve hepsini tek tฤฑkla yeniden aรงฤฑn. Termix son oturumunuzu da hatฤฑrlar, bรถylece sayfayฤฑ yenileseniz de baลŸka cihaza geรงseniz de sekmeleriniz geri gelir. + + + +**Rehberli kurulum:** +Kฤฑsa bir kurulum, arayรผz รถn ayarฤฑnฤฑ, temanฤฑzฤฑ, istediฤŸiniz รถzellikleri ve ilk sunucunuzu seรงmenizde size yol gรถsterir. Basit kip kullanmadฤฑฤŸฤฑnฤฑz ลŸeyleri gizler; kurulumu istediฤŸiniz zaman yeniden รงalฤฑลŸtฤฑrabilir veya รถn ayarฤฑ deฤŸiลŸtirebilirsiniz. + +
+ +**BaฤŸฤฑmsฤฑz masaรผstรผ ve eลŸitleme:** +Masaรผstรผ uygulamasฤฑ kendi arka ucu ve veritabanฤฑyla tek baลŸฤฑna, sunucusuz รงalฤฑลŸฤฑr. ฤฐsterseniz bir Termix sunucusuna baฤŸlayฤฑp sunucularฤฑ, kimlik bilgilerini, parรงacฤฑklarฤฑ ve fazlasฤฑnฤฑ iki yรถnlรผ eลŸitleyebilir, baฤŸlantฤฑlarฤฑn kendi makinenizden mi yoksa sunucu รผzerinden mi kurulacaฤŸฤฑnฤฑ seรงebilirsiniz. + + + +**Komut satฤฑrฤฑ:** +KabuฤŸunuz ve betikleriniz iรงin bir `termix` CLI'ฤฑ. Terminal aรงฤฑn, tek bir sunucuda veya tรผm filoda komut รงalฤฑลŸtฤฑrฤฑn, SFTP ile dosya taลŸฤฑyฤฑn ve sunucularฤฑ, parรงacฤฑklarฤฑ ve kimlik bilgilerini yรถnetin. `npm install -g @termix-cli/cli` ile kurun ya da baฤŸฤฑmsฤฑz bir รงalฤฑลŸtฤฑrฤฑlabilir dosya edinin. [CLI belgelerine](https://docs.termix.site/cli) bakฤฑn. + +
+ +**Gรผvenlik:** +Parolalar, anahtarlar ve diฤŸer gizli bilgiler kullanฤฑcฤฑ bazฤฑnda ลŸifrelenir, veritabanฤฑ dosyalarฤฑnฤฑn kendisi de diskte ลŸifrelenebilir. Nasฤฑl รงalฤฑลŸtฤฑฤŸฤฑ iรงin [belgelere](https://docs.termix.site/security) bakฤฑn. **Diller:** -Yaklasik 30 dil icin yerlesik destek ([Crowdin](https://docs.termix.site/translations) tarafindan yonetilir). +YaklaลŸฤฑk 30 dil yerleลŸik olarak gelir, [Crowdin](https://docs.termix.site/translations) รผzerinden yรถnetilir.
- + - + - + - + @@ -252,9 +311,9 @@ Yaklasik 30 dil icin yerlesik destek ([Crowdin](https://docs.termix.site/transla ## Kurulum -Termix'i tum platformlara nasil kuracaginiz hakkinda daha fazla bilgi icin Termix [Belgelerine](https://docs.termix.site/install) bakin. +Tรผm platformlar iรงin ayrฤฑntฤฑlฤฑ kurulum yรถnergelerini [Termix belgelerinde](https://docs.termix.site/install) bulabilirsiniz. -Ornek bir Docker Compose dosyasi (uzak masaustu ozelliklerini kullanmayi planlamiyorsaniz `guacd` ve agi cikarabilirsiniz): +ร–rnek Docker Compose dosyasฤฑ (uzak masaรผstรผnรผ kullanmayacaksanฤฑz `guacd` ve aฤŸ kฤฑsmฤฑnฤฑ รงฤฑkarabilirsiniz): ```yaml services: @@ -291,19 +350,45 @@ networks: driver: bridge ``` +### Komut satฤฑrฤฑ + +Termix'in bir CLI'ฤฑ da var; sunucularฤฑnฤฑzฤฑ terminalden yรถnetebilir ve Termix'i kendi betiklerinizde kullanabilirsiniz. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +Terminal aรงabilir, tek bir sunucuda veya tรผm filoda komut รงalฤฑลŸtฤฑrabilir, SFTP ile dosya taลŸฤฑyabilir ve sunucularฤฑ, parรงacฤฑklarฤฑ ve kimlik bilgilerini yรถnetebilir. Belgelerin tamamฤฑ [docs.termix.site/cli](https://docs.termix.site/cli) adresinde. + +### Bulutta barฤฑndฤฑrma + +Termix sunucusunu kendi aฤŸฤฑnฤฑz yerine bir VPS รผzerinde de รงalฤฑลŸtฤฑrabilirsiniz. Termix yรถnettiฤŸi aฤŸฤฑn iรงinde รงalฤฑลŸฤฑyorsa, bir kesinti onu da beraberinde gรถtรผrรผr; hem de tam onu tamir iรงin kullanmanฤฑz gereken anda. DฤฑลŸarฤฑda รงalฤฑลŸtฤฑrmak eriลŸilebilir kalmasฤฑnฤฑ saฤŸlar, sabit bir IP verir ve VPN ya da port yรถnlendirme olmadan her yerden girmenize izin verir. + +[GINERNET](https://docs.termix.site/install/ginernet) Termix'e sponsor oluyor ve belgelerde onlarฤฑn VPS platformuna kurulum iรงin adฤฑm adฤฑm bir rehber var. +
-## BaฤŸฤฑลŸ Yapฤฑn +## Telemetri -Termix รผcretsiz ve aรงฤฑk kaynaklฤฑdฤฑr, abonelik veya รผcretli plan yoktur. Faydalฤฑ buluyorsaniz, sunucu maliyetleri, alan adlari ve gelistirme suresine katkida bulunmak icin bagis yapmayi dusunebilirsiniz. Bagislar ayrica SAML, Kubernetes ve Agent destegi gibi ozellikleri gelistirmek icin gereken arastirma ve ogrenme suresini finanse etmeye yardimci olur. Ilerlemeyi takip edin ve asagidan bagis yapin. +Termix gรผnde bir kez kรผรงรผk ve anonim bir sinyal gรถnderir; bรถylece kaรง kurulumun รงalฤฑลŸtฤฑฤŸฤฑnฤฑ ve hangi รถzelliklerin kullanฤฑldฤฑฤŸฤฑnฤฑ gรถrebiliyorum. ฤฐรงinde rastgele bir kurulum kimliฤŸi, kaรง kullanฤฑcฤฑ ve sunucunuz olduฤŸu, uygulama sรผrรผmรผ ve son 24 saatte hangi รถzelliklerin (terminal, dosya yรถneticisi, tรผneller, docker vb.) kullanฤฑldฤฑฤŸฤฑ yer alฤฑr. ฤฐรงinde asla kullanฤฑcฤฑ adlarฤฑ, sunucu adlarฤฑ, IP adresleri, kimlik bilgileri ya da sizi veya sunucularฤฑnฤฑzฤฑ tanฤฑmlayan baลŸka bir ลŸey bulunmaz. -[BaฤŸฤฑลŸ Yapฤฑn](https://donate.termix.site/) +Varsayฤฑlan olarak aรงฤฑktฤฑr. Yรถnetici ayarlarฤฑnda Genel bรถlรผmรผnden kapatabilir ya da Termix'i hiรง baลŸlatmadan รถnce `ENABLE_TELEMETRY=false` tanฤฑmlayabilirsiniz. + +
+ +## BaฤŸฤฑลŸ + +Termix รผcretsiz ve aรงฤฑk kaynaklฤฑdฤฑr; abonelik ya da รผcretli plan yoktur. ฤฐลŸinize yarฤฑyorsa, sunucu, alan adฤฑ ve geliลŸtirme sรผresi masraflarฤฑna katkฤฑ iรงin baฤŸฤฑลŸ yapmayฤฑ dรผลŸรผnรผn. BaฤŸฤฑลŸlar ayrฤฑca SAML, Kubernetes ve aracฤฑ desteฤŸi gibi รถzellikler iรงin gereken araลŸtฤฑrma ve รถฤŸrenme sรผresini karลŸฤฑlar. ฤฐlerlemeyi aลŸaฤŸฤฑdan izleyip baฤŸฤฑลŸ yapabilirsiniz. + +[BaฤŸฤฑลŸ yap](https://donate.termix.site/)
## Sponsorlar -Gelistirmeyi desteklemek icin ucretli bir yerlesim ile ilgileniyor musunuz? [mail@termix.site](mailto:mail@termix.site) adresine e-posta gonderin. +GeliลŸtirmeyi desteklemek iรงin รผcretli bir yerleลŸim ilginizi รงeker mi? [mail@termix.site](mailto:mail@termix.site) adresine yazฤฑn.
@@ -325,10 +410,6 @@ Gelistirmeyi desteklemek icin ucretli bir yerlesim ile ilgileniyor musunuz? [mai Cloudflare     - - Tailscale - -    Akamai @@ -340,18 +421,21 @@ Gelistirmeyi desteklemek icin ucretli bir yerlesim ile ilgileniyor musunuz? [mai Rack Genius - +    + + Ginernet +

## Destek -Termix ile ilgili yardima ihtiyaciniz varsa veya bir ozellik talep etmek istiyorsaniz, [Sorunlar](https://github.com/Termix-SSH/Support/issues) sayfasini ziyaret edin, giris yapin ve `New Issue` butonuna basin. Lutfen sorununuzu mumkun oldugunca ayrintili yazin, tercihen Ingilizce olarak. Ayrica [Discord](https://discord.gg/jVQGdvHDrf) sunucusuna katilabilir ve destek kanalini ziyaret edebilirsiniz, ancak yanit sureleri daha uzun olabilir. +Yardฤฑma mฤฑ ihtiyacฤฑnฤฑz var ya da bir รถzellik mi istiyorsunuz? [Yeni bir konu](https://github.com/Termix-SSH/Support/issues) aรงฤฑn ve olabildiฤŸince ayrฤฑntฤฑ ekleyin, mรผmkรผnse ฤฐngilizce yazฤฑn. [Discord](https://discord.gg/jVQGdvHDrf) รผzerindeki destek kanalฤฑnda da sorabilirsiniz, ancak oradaki yanฤฑtlar daha uzun sรผrebilir.
-## Ekran Goruntuleri +## Ekran gรถrรผntรผleri
@@ -359,7 +443,7 @@ Termix ile ilgili yardima ihtiyaciniz varsa veya bir ozellik talep etmek istiyor [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -YouTube'da guncelleme ozetlerini izleyin +Gรผncelleme tanฤฑtฤฑmlarฤฑnฤฑ YouTube'da izleyin

@@ -399,18 +483,18 @@ Termix ile ilgili yardima ihtiyaciniz varsa veya bir ozellik talep etmek istiyor
PlatformDagitimDaฤŸฤฑtฤฑm
WebHerhangi bir modern tarayici (Chrome, Safari, Firefox) ยท PWA destegiGรผncel her tarayฤฑcฤฑ (Chrome, Safari, Firefox) ยท PWA desteฤŸi
Windows x64/ia32Tasฤฑnabilir ยท MSI Yukleyici ยท ChocolateyTaลŸฤฑnabilir ยท MSI kurulumu ยท Chocolatey
Linux x64/ia32Tasฤฑnabilir ยท AUR ยท AppImage ยท Deb ยท FlatpakTaลŸฤฑnabilir ยท AUR ยท AppImage ยท Deb ยท Flatpak
macOS x64/ia32, v12.0+
-Bazi videolar ve gorseller guncel olmayabilir veya ozellikleri tam olarak yansitmayabilir. +Bazฤฑ videolar ve gรถrseller gรผncelliฤŸini yitirmiลŸ ya da รถzellikleri tam olarak gรถstermiyor olabilir.
-## Planlanan Ozellikler +## Planlanan รถzellikler -Tum planlanan ozellikler icin [Projeler](https://github.com/orgs/Termix-SSH/projects/5) sayfasina bakin. Katkida bulunmak istiyorsaniz, [Katkida Bulunma](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) sayfasina bakin. +Planlanan tรผm รถzellikler [Projects](https://github.com/orgs/Termix-SSH/projects/5) sayfasฤฑnda. Katkฤฑda bulunmak isterseniz [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) dosyasฤฑna bakฤฑn.
## Lisans -Apache Lisansi Surumu 2.0 altinda dagitilmaktadir. Daha fazla bilgi icin `LICENSE` dosyasina bakin. +Apache Lisansฤฑ Sรผrรผm 2.0 ile daฤŸฤฑtฤฑlฤฑr. Ayrฤฑntฤฑlar iรงin `LICENSE` dosyasฤฑna bakฤฑn. diff --git a/docs/readme/README-VI.md b/docs/readme/README-VI.md index 106b48c..2831aa0 100644 --- a/docs/readme/README-VI.md +++ b/docs/readme/README-VI.md @@ -4,7 +4,7 @@

Termix

-

Quan ly SSH tu luu tru va truy cap may tinh tu xa

+

Quแบฃn lรฝ mรกy chแปง tแปฑ lฦฐu trแปฏ, tแปซ SSH vร  mรกy tรญnh tแปซ xa cho ฤ‘แบฟn tแปฑ ฤ‘แป™ng hoรก

English ยท @@ -37,7 +37,7 @@
-Termix lร  dแปฑ รกn miแป…n phรญ vร  mรฃ nguแป“n mแปŸ. Nแบฟu bแบกn thแบฅy hแปฏu รญch, hรฃy cรขn nhแบฏc [quyรชn gรณp](https://donate.termix.site/) ฤ‘แปƒ giรบp trang trแบฃi chi phรญ mรกy chแปง vร  thแปi gian phรกt triแปƒn. +Termix miแป…n phรญ vร  mรฃ nguแป“n mแปŸ. Nแบฟu bแบกn thแบฅy hแปฏu รญch, hรฃy cรขn nhแบฏc [quyรชn gรณp](https://donate.termix.site/) ฤ‘แปƒ giรบp trang trแบฃi chi phรญ mรกy chแปง vร  thแปi gian phรกt triแปƒn.
@@ -49,145 +49,201 @@ Termix lร  dแปฑ รกn miแป…n phรญ vร  mรฃ nguแป“n mแปŸ. Nแบฟu bแบกn thแบฅy hแปฏu

Repo of the Day Achievement
- Dat duoc vao ngay 1 thang 9 nam 2025 + ฤแบกt ฤ‘ฦฐแปฃc vร o ngร y 1 thรกng 9 nฤƒm 2025


-## Tong Quan +## Tแป•ng quan -Termix la nen tang quan ly may chu tat ca trong mot, ma nguon mo, mien phi vinh vien, tu luu tru. No cung cap giai phap da nen tang de quan ly may chu va co so ha tang cua ban thong qua mot giao dien truc quan duy nhat. Termix cung cap quyen truy cap terminal SSH, dieu khien may tinh tu xa (RDP, VNC, Telnet), kha nang tao duong ham SSH, quan ly tep tu xa va nhieu cong cu khac. Termix la giai phap thay the mien phi va tu luu tru hoan hao cho Termius, kha dung tren tat ca cac nen tang. +Termix lร  nแปn tแบฃng miแป…n phรญ, mรฃ nguแป“n mแปŸ, tแปฑ lฦฐu trแปฏ ฤ‘แปƒ quแบฃn lรฝ mรกy chแปง cแปงa bแบกn. Nรณ gom vร o mแป™t chแป— terminal SSH, mรกy tรญnh tแปซ xa (RDP, VNC, Telnet), truyแปn tแป‡p, tunnel, Docker, sแป‘ liแป‡u vร  tแปฑ ฤ‘แป™ng hoรก, trรชn web, mรกy tรญnh vร  ฤ‘iแป‡n thoแบกi. ฤรขy lร  bแบฃn thay thแบฟ tแปฑ lฦฐu trแปฏ cho Termius vร  sแบฝ miแป…n phรญ mรฃi mรฃi.
-## Tinh Nang +## Tรญnh nฤƒng + + + + + + + + + + + + + + + + @@ -196,43 +252,46 @@ Ho tro tich hop khoang 30 ngon ngu (duoc quan ly boi [Crowdin](https://docs.term
-Them tinh nang +Thรชm tรญnh nฤƒng khรกc
-- **Bang Dieu Khien** - Xem thong tin may chu trong nhรกy mat tren bang dieu khien cua ban -- **Khoa API** - Tao khoa API theo pham vi nguoi dung voi ngay het han de su dung cho tu dong hoa/CI -- **Xuat/Nhap Du Lieu** - Xuat va nhap may chu SSH, thong tin xac thuc va du lieu trinh quan ly tep -- **Thiet Lap SSL Tu Dong** - Tao va quan ly chung chi SSL tich hop voi chuyen huong HTTPS -- **Giao Dien Hien Dai** - Giao dien sach se, than thien voi may tinh/di dong duoc xay dung bang React, Tailwind CSS va Shadcn. Chon giua nhieu chu de UI khac nhau bao gom sang, toi, Dracula, v.v. Su dung duong dan URL de mo bat ky ket noi nao o che do toan man hinh. -- **Lich Su Lenh** - Tu dong hoan thanh va xem cac lenh SSH da chay truoc do -- **Ket Noi Nhanh** - Ket noi den may chu ma khong can luu du lieu ket noi -- **Bang Lenh** - Nhan dup phim shift trai de truy cap nhanh cac ket noi SSH bang ban phim -- **Tich Hop Proxmox** - Tu dong them may chu vao Termix tu instance Proxmox cua ban -- **SSH Giau Tinh Nang** - Ho tro jump host, Warpgate, ket noi dua tren TOTP, SOCKS5, xac minh khoa may chu, tu dong dien mat khau, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, ghi nhat ky terminal, chuyen tiep SSH agent, Bitwarden SSH agent, ky SSH bang HashiCorp Vault va nhieu hon nua. -- **Termix ID** - Mot tuong duong cua sshid.io duoc tich hop san trong Termix. Dang ky mot ten dinh danh, cong bo khoa SSH cong khai cua ban tai mot URL phan giai va su dung CA tich hop san de cap chung chi SSH. +- **Bแบฃng ฤ‘iแปu khiแปƒn** - Nhรฌn nhanh toร n bแป™ mรกy chแปง, vแป›i cรกc thแบป do bแบกn tแปฑ sแบฏp xแบฟp +- **Sฦก ฤ‘แป“ mแบกng** - Vแบฝ homelab cแปงa bแบกn tแปซ danh sรกch mรกy chแปง, kรจm trแบกng thรกi theo thแปi gian thแปฑc +- **Theo dรตi tmux** - Xem cรกc phiรชn, cแปญa sแป• vร  khung tmux, cรณ xem trฦฐแป›c vร  tรฌm kiแบฟm +- **Khoรก API** - Khoรก theo tแปซng ngฦฐแปi dรนng cรณ ngร y hแบฟt hแบกn, dรนng cho script vร  CI +- **Xuแบฅt vร  nhแบญp** - Chuyแปƒn mรกy chแปง, thรดng tin ฤ‘ฤƒng nhแบญp vร  dแปฏ liแป‡u trรฌnh quแบฃn lรฝ tแป‡p ra vร o +- **SSL tแปฑ ฤ‘แป™ng** - Chแปฉng chแป‰ ฤ‘ฦฐแปฃc tแบกo vร  gia hแบกn giรบp bแบกn, kรจm chuyแปƒn hฦฐแป›ng HTTPS, hoแบทc dรนng chแปฉng chแป‰ cแปงa riรชng bแบกn +- **Cฦก sแปŸ dแปฏ liแป‡u** - Mแบทc ฤ‘แป‹nh lร  SQLite, ฤ‘แป“ng thแปi hแป— trแปฃ PostgreSQL vร  MySQL +- **Giao diแป‡n hiแป‡n ฤ‘แบกi** - Giao diแป‡n React gแปn gร ng chแบกy tแป‘t trรชn mรกy tรญnh vร  ฤ‘iแป‡n thoแบกi, vแป›i cรกc chแปง ฤ‘แป nhฦฐ sรกng, tแป‘i vร  Dracula. Mแปi kแบฟt nแป‘i ฤ‘แปu mแปŸ toร n mร n hรฌnh ฤ‘ฦฐแปฃc tแปซ mแป™t ฤ‘แป‹a chแป‰ +- **Bแบฃng lแป‡nh** - Nhแบฅn hai lแบงn phรญm Shift trรกi ฤ‘แปƒ nhแบฃy tแป›i mแป™t mรกy chแปง bแบฑng bร n phรญm +- **Phรญm tแบฏt** - Chuyแปƒn giแปฏa cรกc thแบป, ฤ‘รณng thแบป vร  nhiแปu thao tรกc khรกc, ฤ‘แปu gรกn lแบกi ฤ‘ฦฐแปฃc +- **Wake-on-LAN** - ฤรกnh thแปฉc mแป™t mรกy tแปซ Termix hoแบทc tแปซ mแป™t bฦฐแป›c tแปฑ ฤ‘แป™ng hoรก +- **Xรกc thแปฑc qua proxy tin cแบญy** - ฤแปƒ reverse proxy lo phแบงn ฤ‘ฤƒng nhแบญp rแป“i chuyแปƒn thรดng tin ngฦฐแปi dรนng vร o +- **SSH nhiแปu tรญnh nฤƒng** - Mรกy chแปง trung gian, Warpgate, hแปi mรฃ TOTP, SOCKS5, kiแปƒm tra khoรก mรกy chแปง, tแปฑ ฤ‘iแปn mแบญt khแบฉu, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, ghi nhแบญt kรฝ terminal, chuyแปƒn tiแบฟp agent, SSH agent cแปงa Bitwarden, kรฝ SSH bแบฑng HashiCorp Vault vร  nhiแปu thแปฉ khรกc +- **Termix ID** - Bแบฃn dแปฑng sแบตn theo kiแปƒu sshid.io. ฤฤƒng kรฝ mแป™t tรชn, cรดng bแป‘ khoรก cรดng khai cแปงa bแบกn tแบกi mแป™t ฤ‘แป‹a chแป‰ phรขn giแบฃi, vร  cแบฅp chแปฉng chแป‰ SSH tแปซ CA tรญch hแปฃp

-## Ho Tro Nen Tang +## Nแปn tแบฃng hแป— trแปฃ
-**Truy Cap Terminal SSH:** -Terminal day du tinh nang voi ho tro chia man hinh (len den 4 bang) voi he thong tab kieu trinh duyet. Bao gom ho tro tuy chinh terminal bao gom cac chu de terminal pho bien, phong chu va cac thanh phan khac. +**Terminal SSH:** +Mแป™t terminal ฤ‘แบงy ฤ‘แปง vแป›i cรกc thแบป giแป‘ng trรฌnh duyแป‡t vร  chia ฤ‘รดi mร n hรฌnh, tแป‘i ฤ‘a 6 khung cรนng lรบc. Bแบกn tแปฑ chแปn giao diแป‡n, phรดng chแปฏ vร  mร u sแบฏc. Phรญa trรชn mแป—i phiรชn cรณ mแป™t thanh cรดng cแปฅ hiแป‡n CPU, bแป™ nhแป› vร  แป• ฤ‘ฤฉa theo thแปi gian thแปฑc, kรจm lแป‘i tแบฏt tแป›i tแป‡p, Docker, tunnel vร  sแป‘ liแป‡u cแปงa mรกy chแปง ฤ‘รณ. -**Truy Cap Man Hinh Tu Xa:** -Ho tro RDP, VNC va Telnet qua trinh duyet voi day du tuy chinh va chia man hinh. +**Mรกy tรญnh tแปซ xa:** +RDP, VNC vร  Telnet ngay trong trรฌnh duyแป‡t, dรนng thแบป vร  chia ฤ‘รดi mร n hรฌnh nhฦฐ mแปi phiรชn khรกc. Cรณ trรฌnh duyแป‡t tแป‡p cho แป• ฤ‘ฤฉa RDP vร  tแบฃi lรชn bแบฑng cรกch kรฉo thแบฃ. Trรชn mรกy tรญnh Windows, bแบกn cรฒn cรณ thแปƒ mแปŸ mรกy chแปง bแบฑng แปฉng dแปฅng RDP cแปงa hแป‡ ฤ‘iแปu hร nh.
-**Quan Ly Duong Ham SSH:** -Tao va quan ly duong ham SSH giua cac may chu voi tu dong ket noi lai, giam sat suc khoe va chuyen tiep cuc bo, tu xa hoac SOCKS dong. Cai dat duong ham tu may khach desktop den may chu duoc luu tru cuc bo cho moi ban cai dat desktop; cac snapshot C2S preset tuy chon co the duoc luu tren may chu, doi ten, tai hoac xoa khi ban muon di chuyen mot cau hinh duong ham cuc bo giua cac may khach. +**Tunnel SSH:** +Chuyแปƒn tiแบฟp cแปฅc bแป™, tแปซ xa vร  SOCKS ฤ‘แป™ng, cรณ tแปฑ kแบฟt nแป‘i lแบกi vร  kiแปƒm tra tรฌnh trแบกng. Tunnel tแปซ mรกy khรกch tแป›i mรกy chแปง trong แปฉng dแปฅng mรกy tรญnh ฤ‘ฦฐแปฃc lฦฐu ngay trรชn mรกy ฤ‘รณ, vร  bแบกn cรณ thแปƒ lฦฐu cแบฅu hรฌnh sแบตn lรชn mรกy chแปง ฤ‘แปƒ mang sang mรกy khรกc. -**Trinh Quan Ly Tep Tu Xa:** -Quan ly tep truc tiep tren may chu tu xa voi ho tro xem va chinh sua ma, hinh anh, am thanh va video. Tai len, tai xuong, doi ten, xoa va di chuyen tep lien mach voi ho tro sudo. Bao gom ho tro di chuyen tep tu may chu nay sang may chu khac. +**Trรฌnh quแบฃn lรฝ tแป‡p:** +Duyแป‡t, sแปญa, tแบฃi lรชn, tแบฃi xuแป‘ng, ฤ‘แป•i tรชn, di chuyแปƒn vร  xoรก tแป‡p qua SFTP, cรณ hแป— trแปฃ sudo. Xem vร  sแปญa mรฃ nguแป“n, hรฌnh แบฃnh, รขm thanh vร  video. Sao chรฉp tแป‡p thแบณng tแปซ mรกy chแปง nร y sang mรกy chแปง khรกc, hแป‡ thแป‘ng tแปฑ chแปn ฤ‘ฦฐแปng nhanh nhแบฅt vร  kiแปƒm tra tรญnh toร n vแบนn khi truyแปn.
-**Quan Ly Docker va Podman:** -Khoi dong, dung, tam dung, xoa container. Xem thong ke container. Dieu khien container bang terminal docker exec. Ho tro ca Docker va Podman lam moi truong chay container. Khong duoc tao ra de thay the Portainer hay Dockge ma don gian la de quan ly container cua ban thay vi tao moi chung. +**Docker vร  Podman:** +KhแปŸi ฤ‘แป™ng, dแปซng, tแบกm dแปซng vร  xoรก container, xem thรดng sแป‘ cแปงa chรบng vร  mแปŸ mแป™t shell bรชn trong. Chแบกy ฤ‘ฦฐแปฃc vแป›i cแบฃ Docker lแบซn Podman. Nรณ khรดng nhแบฑm thay thแบฟ Portainer hay Dockge, chแป‰ ฤ‘แปƒ quแบฃn lรฝ nhแปฏng container bแบกn ฤ‘รฃ cรณ. -**Trinh Quan Ly May Chu SSH:** -Luu, sap xep va quan ly cac ket noi SSH cua ban voi the va thu muc (ho tro tuy chinh thu muc va thu muc long nhau), de dang luu thong tin dang nhap co the tai su dung dong thoi co the tu dong hoa viec trien khai khoa SSH. +**Quแบฃn lรฝ mรกy chแปง:** +Lฦฐu vร  sแบฏp xแบฟp mรกy chแปง bแบฑng thแบป vร  thฦฐ mแปฅc lแป“ng nhau mร  bแบกn cรณ thแปƒ ฤ‘แบทt tรชn vร  tรด mร u. Dรนng lแบกi thรดng tin ฤ‘ฤƒng nhแบญp ฤ‘รฃ lฦฐu cho nhiแปu mรกy chแปง, tแปฑ ฤ‘แป™ng triแปƒn khai khoรก SSH, gom mรกy chแปง dฦฐแป›i mแป™t mรกy chแปง cha, sแปญa vร  xuแบฅt hร ng loแบกt, vร  dรนng kแบฟt nแป‘i nhanh cho nhแปฏng lแบงn kแบฟt nแป‘i mแป™t lแบงn mร  bแบกn khรดng muแป‘n lฦฐu.
-**Chi So May Chu:** -Xem muc su dung CPU, bo nho, o dia, mang, thoi gian hoat dong, thong tin he thong, tuong lua, giam sat cong, trinh xem nhat ky, nguoi dung/quyen, chung chi va nhieu hon nua tren hau het cac may chu chay Linux. Bao gom bieu do lich su theo chuoi thoi gian va canh bao dua tren nguong voi ho tro ntfy va webhook. +**Sแป‘ liแป‡u mรกy chแปง:** +CPU, bแป™ nhแป›, แป• ฤ‘ฤฉa, mแบกng, nhiแป‡t ฤ‘แป™, thแปi gian hoแบกt ฤ‘แป™ng, tiแบฟn trรฌnh, cแป•ng, lฦฐแปฃt ฤ‘ฤƒng nhแบญp vร  thรดng tin hแป‡ thแป‘ng trรชn hแบงu hแบฟt mรกy chแปง Linux, kรจm biแปƒu ฤ‘แป“ lแป‹ch sแปญ. Cรกc thแบป quแบฃn lรฝ cho phรฉp bแบกn xแปญ lรฝ dแป‹ch vแปฅ, tรกc vแปฅ cron, gรณi phแบงn mแปm, ngฦฐแปi dรนng, luแบญt tฦฐแปng lแปญa, WireGuard, Tailscale, chแปฉng chแป‰ SSL, nhแบญt kรฝ vร  kiแปƒm tra tรฌnh trแบกng mร  khรดng cแบงn rแปi Termix. -**Xac Thuc Nguoi Dung:** -Quan ly nguoi dung an toan voi quyen quan tri (co the chinh sua thong tin cua nguoi dung khac) va ho tro OIDC/LDAP/SSO (co kiem soat truy cap), 2FA (TOTP) va passkey (WebAuthn). Xem phien hoat dong cua nguoi dung tren tat ca cac nen tang va thu hoi quyen. Lien ket tai khoan OIDC/Noi bo cua ban voi nhau. Xem nhat ky kiem toan cac hanh dong cua tat ca nguoi dung. +**Tแปฑ ฤ‘แป™ng hoรก:** +Chแปn mแป™t ฤ‘iแปu kiแป‡n kรญch hoแบกt, rแป“i nรณi bแบกn muแป‘n ฤ‘iแปu gรฌ xแบฃy ra. ฤiแปu kiแป‡n gแป“m mแป™t sแป‘ liแป‡u vฦฐแปฃt ngฦฐแปกng, mแป™t mรกy chแปง sแบญp hoแบทc sแป‘ng lแบกi, kiแปƒm tra tรฌnh trแบกng thay ฤ‘แป•i, mแป™t lแป‹ch ฤ‘แป‹nh sแบตn, mแป™t sแปฑ kiแป‡n container, hoแบทc mแป™t webhook gแปญi ฤ‘แบฟn. Cรกc bฦฐแป›c cรณ thแปƒ chแบกy lแป‡nh vร  ฤ‘oแบกn lแป‡nh, ฤ‘iแปu khiแปƒn container vร  tunnel, ฤ‘รกnh thแปฉc mรกy chแปง, gแปi mแป™t ฤ‘แป‹a chแป‰, chแป, rแบฝ nhรกnh theo ฤ‘iแปu kiแป‡n, chแบกy mแป™t tแปฑ ฤ‘แป™ng hoรก khรกc, vร  bรกo cho bแบกn qua ntfy, Discord hoแบทc webhook. Chแบกy thแปญ giรบp bแบกn kiแปƒm tra an toร n trฦฐแป›c.
-**Tich Hop Tailscale:** -Liet ke cac thiet bi trong mang Tailscale de nhanh chong them vao lam may chu, va ket noi bang Tailscale SSH lam phuong thuc xac thuc, de cac ACL mang xu ly uy quyen ma khong can luu tru thong tin xac thuc. +**Nhรณm mรกy chแปง:** +Gom mรกy chแปง vร o mแป™t nhรณm bแบฑng cรกch tแปฑ chแปn hoแบทc theo luแบญt thแบป, ฤ‘แปƒ mรกy chแปง mแป›i tแปฑ vร o nhรณm. Chแบกy mแป™t lแป‡nh trรชn mแปi mรกy chแปง cรนng lรบc, ฤ‘แบฉy vร  lแบฅy tแป‡p trรชn tแบฅt cแบฃ, cร i gรณi phแบงn mแปm, vร  thu thแบญp danh sรกch hแป‡ ฤ‘iแปu hร nh, nhรขn, kiแบฟn trรบc vร  thแปi gian hoแบกt ฤ‘แป™ng. -**RBAC/Chia Se:** -Tao vai tro va chia se may chu giua nguoi dung/vai tro. Ho tro tat ca cac loai xac thuc va tat ca cac giao thuc may chu. +**Trแปฃ lรฝ AI:** +Lร  tuแปณ chแปn, vร  tแบฏt cho ฤ‘แบฟn khi bแบกn tแปฑ bแบญt. Kแบฟt nแป‘i OpenAI, Anthropic, Gemini, Ollama hoแบทc bแบฅt kแปณ endpoint tฦฐฦกng thรญch OpenAI nร o rแป“i hแปi vแป hแป‡ thแป‘ng cแปงa bแบกn. Nรณ ฤ‘แปc ฤ‘ฦฐแปฃc mรกy chแปง, nhรณm mรกy chแปง, ฤ‘oแบกn lแป‡nh vร  cแบฃnh bรกo, vร  ฤ‘แป xuแบฅt thay ฤ‘แป•i ฤ‘แปƒ bแบกn duyแป‡t chแปฉ khรดng tแปฑ lร m. Nรณ khรดng bao giแป chแบกm ฤ‘ฦฐแปฃc vร o thรดng tin ฤ‘ฤƒng nhแบญp, ngฦฐแปi dรนng hay thiแบฟt lแบญp. Quแบฃn trแป‹ viรชn cรณ thแปƒ tแบฏt hแบณn cho cแบฃ hแป‡ thแป‘ng, cรฒn bแบกn cรณ thแปƒ แบฉn nรณ ngay khi cร i ฤ‘แบทt ban ฤ‘แบงu.
-**Ket Noi Noi Tiep:** -Ket noi voi cac thiet bi noi tiep (router, switch, vi dieu khien, v.v.) truc tiep tu trinh duyet hoac ung dung may tinh. Cau hinh toc do baud, bit du lieu, bit dung va chan le. Su dung Web Serial API tren trinh duyet duoc ho tro hoac backend ban dia trong ung dung Electron. +**ฤฤƒng nhแบญp vร  ngฦฐแปi dรนng:** +Tร i khoแบฃn cแปฅc bแป™ cรนng vแป›i ฤ‘ฤƒng nhแบญp qua OIDC, LDAP, GitHub vร  Google, kรจm xรกc thแปฑc hai bฦฐแป›c (TOTP), passkey (WebAuthn) vร  thiแบฟt bแป‹ tin cแบญy. Quแบฃn trแป‹ viรชn cรณ thแปƒ quแบฃn lรฝ ngฦฐแปi dรนng, รกnh xแบก nhรณm OIDC sang vai trรฒ, xem mแปi phiรชn ฤ‘ang hoแบกt ฤ‘แป™ng trรชn mแปi nแปn tแบฃng vร  thu hแป“i chรบng. Bแบกn cรณ thแปƒ liรชn kแบฟt tร i khoแบฃn cแปฅc bแป™ vแป›i tร i khoแบฃn OIDC, vร  xem nhแบญt kรฝ kiแปƒm toรกn vแป nhแปฏng gรฌ mแปi ngฦฐแปi ฤ‘รฃ lร m. -**Canh Bao:** -Dat cac quy tac canh bao dua tren nguong cho chi so may chu (CPU, bo nho, o dia, v.v.) va nhan thong bao qua ntfy hoac webhook khi chung kich hoat. Xem canh bao dang kich hoat va da giai quyet trong nhat ky lich su. +**Vai trรฒ vร  chia sแบป:** +Tแบกo vai trรฒ vร  chia sแบป mรกy chแปง vแป›i ngฦฐแปi dรนng hoแบทc vai trรฒ theo bแป‘n mแปฉc: kแบฟt nแป‘i, xem, sแปญa vร  quแบฃn lรฝ. Hoแบกt ฤ‘แป™ng vแป›i mแปi kiแปƒu xรกc thแปฑc vร  mแปi giao thแปฉc, vร  bแบกn cรณ thแปƒ thay thรดng tin ฤ‘ฤƒng nhแบญp dรนng cho mแป™t mรกy chแปง ฤ‘ฦฐแปฃc chia sแบป.
-**Trang Chu:** -Trang chu co the tuy chinh hoan toan voi luoi widget keo va tha. Them widget cho trang thai may chu, lien ket dich vu, dong ho, ghi chu, feed RSS, thoi tiet, container Docker, bieu do chi so may chu, terminal nhung, iframe va nhieu hon nua. +**Cแบฃnh bรกo:** +ฤแบทt luแบญt cho cรกc sแป‘ liแป‡u mรกy chแปง nhฦฐ CPU, bแป™ nhแป› vร  แป• ฤ‘ฤฉa, rแป“i nhแบญn thรดng bรกo qua ntfy, Discord hoแบทc webhook khi chรบng kรญch hoแบกt. Xem cแบฃnh bรกo ฤ‘ang bแบญt vร  ฤ‘รฃ hแบฟt trong nhแบญt kรฝ, vร  bแป qua nhแปฏng cรกi bแบกn khรดng quan tรขm. -**Ma Hoa Co So Du Lieu:** -Backend duoc luu tru duoi dang tep co so du lieu SQLite duoc ma hoa. Xem [tai lieu](https://docs.termix.site/security) de biet them. +**Trang chแปง:** +Mแป™t lฦฐแป›i tiแป‡n รญch kรฉo thแบฃ do bแบกn tแปฑ dแปฑng. Cรณ tiแป‡n รญch cho tรฌnh trแบกng mรกy chแปง, ping, liรชn kแบฟt dแป‹ch vแปฅ, dแบฅu trang, tรฌm kiแบฟm, ฤ‘แป“ng hแป“, lแป‹ch, ฤ‘แบฟm ngฦฐแปฃc, ghi chรบ, RSS, thแปi tiแบฟt, hรฌnh แบฃnh, iframe, Docker, tunnel, biแปƒu ฤ‘แป“ sแป‘ liแป‡u, API riรชng, vร  cแบฃ mแป™t terminal ฤ‘ang chแบกy.
-**Bieu Do Mang:** -Tuy chinh Bang Dieu Khien de truc quan hoa homelab cua ban dua tren cac ket noi SSH voi ho tro trang thai. +**ฤoแบกn lแป‡nh vร  cรดng cแปฅ:** +Lฦฐu nhแปฏng lแป‡nh bแบกn hay dรนng vร  chแบกy chแป‰ vแป›i mแป™t cรบ nhแบฅp, cรณ biแบฟn cho mรกy chแปง vร  cho phแบงn bแบกn tแปฑ nhแบญp. Chแบกy mแป™t lแป‡nh trรชn tแบฅt cแบฃ terminal ฤ‘ang mแปŸ, vร  tรฌm trong lแป‹ch sแปญ lแป‡nh vแป›i gแปฃi รฝ tแปฑ ฤ‘แป™ng. -**Cong Cu SSH:** -Tao doan lenh co the tai su dung, thuc thi chi voi mot cu nhap chuot. Chay mot lenh dong thoi tren nhieu terminal dang mo. +**Chia sแบป phiรชn:** +Chia sแบป trแปฑc tiแบฟp mแป™t phiรชn terminal, RDP, VNC hoแบทc Telnet. Gแปญi mแป™t liรชn kแบฟt mร  ai cลฉng vร o ฤ‘ฦฐแปฃc khรดng cแบงn tร i khoแบฃn, hoแบทc chia sแบป vแป›i mแป™t ngฦฐแปi dรนng Termix cแปฅ thแปƒ, แปŸ chแบฟ ฤ‘แป™ chแป‰ xem hoแบทc cho phรฉp thao tรกc. Chia sแบป cรณ thแปƒ tแปฑ hแบฟt hแบกn hoแบทc bแป‹ thu hแป“i bแบฅt cแปฉ lรบc nร o, vร  cรณ thแปƒ tแบฏt toร n bแป™ hoแบทc theo tแปซng mรกy chแปง.
-**Tab Lien Tuc:** -Cac phien SSH va tab van mo tren cac thiet bi/lan lam moi neu duoc bat trong ho so nguoi dung. +**Ghi phiรชn vร  nhแบญt kรฝ:** +Ghi lแบกi phiรชn terminal, RDP vร  VNC rแป“i xem lแบกi sau. Tแบฃi nhแบญt kรฝ dแบกng vฤƒn bแบฃn cแปงa mแป™t phiรชn, vร  xem nhแบญt kรฝ kแบฟt nแป‘i ฤ‘แปƒ biแบฟt chรญnh xรกc chuyแป‡n gรฌ ฤ‘รฃ xแบฃy ra trong lรบc kแบฟt nแป‘i. -**Ngon Ngu:** -Ho tro tich hop khoang 30 ngon ngu (duoc quan ly boi [Crowdin](https://docs.termix.site/translations)). +**Kแบฟt nแป‘i serial:** +Lร m viแป‡c vแป›i thiแบฟt bแป‹ serial nhฦฐ router, switch vร  vi ฤ‘iแปu khiแปƒn tแปซ trรฌnh duyแป‡t hoแบทc แปฉng dแปฅng mรกy tรญnh. ฤแบทt tแป‘c ฤ‘แป™ baud, bit dแปฏ liแป‡u, bit dแปซng vร  bit chแบตn lแบป. Dรนng Web Serial API trรชn cรกc trรฌnh duyแป‡t hแป— trแปฃ, hoแบทc backend gแป‘c trong แปฉng dแปฅng mรกy tรญnh. + +
+ +**Tailscale:** +Lแบฅy thiแบฟt bแป‹ tแปซ tailnet cแปงa bแบกn ฤ‘แปƒ thรชm lร m mรกy chแปง chแป‰ vแป›i vร i cรบ nhแบฅp, vร  kแบฟt nแป‘i bแบฑng Tailscale SSH ฤ‘แปƒ ACL cแปงa tailnet lo phแบงn quyแปn truy cแบญp, khรดng cแบงn lฦฐu thรดng tin ฤ‘ฤƒng nhแบญp. Headscale vร  endpoint tuแปณ chแป‰nh cลฉng dรนng ฤ‘ฦฐแปฃc. + + + +**Proxmox:** +Nhแบญp mรกy chแปง thแบณng tแปซ mแป™t hแป‡ thแป‘ng Proxmox, vร  theo dรตi sแป‘ liแป‡u cแปงa node vร  mรกy แบฃo, gแป“m CPU, bแป™ nhแป› vร  dung lฦฐแปฃng, trong mแป™t thแบป riรชng. + +
+ +**Khรดng gian lร m viแป‡c vร  thแบป:** +Lฦฐu mแป™t bแป™ thแบป cรนng cรกch chia mร n hรฌnh rแป“i mแปŸ lแบกi toร n bแป™ chแป‰ vแป›i mแป™t cรบ nhแบฅp. Termix cลฉng nhแป› phiรชn gแบงn nhแบฅt, nรชn cรกc thแบป cแปงa bแบกn quay lแบกi sau khi tแบฃi lแบกi trang vร  trรชn thiแบฟt bแป‹ khรกc. + + + +**Cร i ฤ‘แบทt cรณ hฦฐแป›ng dแบซn:** +Mแป™t phแบงn cร i ฤ‘แบทt ngแบฏn sแบฝ hฦฐแป›ng bแบกn chแปn kiแปƒu giao diแป‡n, chแปง ฤ‘แป, nhแปฏng tรญnh nฤƒng bแบกn muแป‘n vร  mรกy chแปง ฤ‘แบงu tiรชn. Chแบฟ ฤ‘แป™ ฤ‘ฦกn giแบฃn แบฉn bแป›t nhแปฏng gรฌ bแบกn khรดng dรนng, vร  bแบกn cรณ thแปƒ chแบกy lแบกi phแบงn cร i ฤ‘แบทt hoแบทc ฤ‘แป•i kiแปƒu bแบฅt cแปฉ lรบc nร o. + +
+ +**แปจng dแปฅng mรกy tรญnh ฤ‘แป™c lแบญp vร  ฤ‘แป“ng bแป™:** +แปจng dแปฅng mรกy tรญnh chแบกy ฤ‘แป™c lแบญp vแป›i backend vร  cฦก sแปŸ dแปฏ liแป‡u riรชng, khรดng cแบงn mรกy chแปง. Bแบกn cลฉng cรณ thแปƒ nแป‘i nรณ vแป›i mแป™t mรกy chแปง Termix ฤ‘แปƒ ฤ‘แป“ng bแป™ hai chiแปu mรกy chแปง, thรดng tin ฤ‘ฤƒng nhแบญp, ฤ‘oแบกn lแป‡nh vร  nhiแปu thแปฉ khรกc, vร  chแปn kแบฟt nแป‘i xuแบฅt phรกt tแปซ mรกy cแปงa bแบกn hay ฤ‘i qua mรกy chแปง. + + + +**Dรฒng lแป‡nh:** +Cรดng cแปฅ `termix` cho shell vร  cรกc script cแปงa bแบกn. MแปŸ terminal, chแบกy mแป™t lแป‡nh trรชn mแป™t mรกy chแปง hoแบทc cแบฃ mแป™t nhรณm, chuyแปƒn tแป‡p qua SFTP, vร  quแบฃn lรฝ mรกy chแปง, ฤ‘oแบกn lแป‡nh vร  thรดng tin ฤ‘ฤƒng nhแบญp. Cร i bแบฑng `npm install -g @termix-cli/cli` hoแบทc tแบฃi bแบฃn chแบกy ฤ‘แป™c lแบญp. Xem [tร i liแป‡u CLI](https://docs.termix.site/cli). + +
+ +**Bแบฃo mแบญt:** +Mแบญt khแบฉu, khoรก vร  cรกc thรดng tin bรญ mแบญt khรกc ฤ‘ฦฐแปฃc mรฃ hoรก theo tแปซng ngฦฐแปi dรนng, vร  bแบฃn thรขn cรกc tแป‡p cฦก sแปŸ dแปฏ liแป‡u cลฉng cรณ thแปƒ mรฃ hoรก trรชn แป• ฤ‘ฤฉa. Xem [tร i liแป‡u](https://docs.termix.site/security) ฤ‘แปƒ biแบฟt cรกch hoแบกt ฤ‘แป™ng. + + + +**Ngรดn ngแปฏ:** +Cรณ sแบตn khoแบฃng 30 ngรดn ngแปฏ, quแบฃn lรฝ qua [Crowdin](https://docs.termix.site/translations).
- - + + - + - + - + @@ -250,11 +309,11 @@ Ho tro tich hop khoang 30 ngon ngu (duoc quan ly boi [Crowdin](https://docs.term
-## Cai Dat +## Cร i ฤ‘แบทt -Truy cap [Tai Lieu](https://docs.termix.site/install) Termix de biet them thong tin ve cach cai dat Termix tren tat ca cac nen tang. +Xem [tร i liแป‡u Termix](https://docs.termix.site/install) ฤ‘แปƒ cรณ hฦฐแป›ng dแบซn cร i ฤ‘แบทt ฤ‘แบงy ฤ‘แปง trรชn mแปi nแปn tแบฃng. -Tep Docker Compose mau (ban co the bo qua `guacd` va mang neu khong co y dinh su dung cac tinh nang dieu khien may tinh tu xa): +Tแป‡p Docker Compose mแบซu (bแบกn cรณ thแปƒ bแป `guacd` vร  phแบงn mแบกng nแบฟu khรดng ฤ‘แป‹nh dรนng mรกy tรญnh tแปซ xa): ```yaml services: @@ -291,19 +350,45 @@ networks: driver: bridge ``` +### Dรฒng lแป‡nh + +Termix cลฉng cรณ CLI, ฤ‘แปƒ bแบกn quแบฃn lรฝ mรกy chแปง tแปซ terminal vร  dรนng Termix trong script cแปงa mรฌnh. + +```bash +npm install -g @termix-cli/cli +termix login --url https://termix.example.com +termix ssh 1 +``` + +Nรณ mแปŸ ฤ‘ฦฐแปฃc terminal, chแบกy lแป‡nh trรชn mแป™t mรกy chแปง hoแบทc cแบฃ mแป™t nhรณm, chuyแปƒn tแป‡p qua SFTP, vร  quแบฃn lรฝ mรกy chแปง, ฤ‘oแบกn lแป‡nh vร  thรดng tin ฤ‘ฤƒng nhแบญp. Tร i liแป‡u ฤ‘แบงy ฤ‘แปง แปŸ [docs.termix.site/cli](https://docs.termix.site/cli). + +### Chแบกy trรชn cloud + +Bแบกn cรณ thแปƒ chแบกy mรกy chแปง Termix trรชn VPS thay vรฌ trong mแบกng cแปงa mรฌnh. Nแบฟu Termix chแบกy ngay trong mแบกng mร  nรณ quแบฃn lรฝ, mแป™t sแปฑ cแป‘ sแบฝ kรฉo nรณ sแบญp theo, ฤ‘รบng lรบc bแบกn cแบงn nรณ ฤ‘แปƒ sแปญa. Chแบกy แปŸ ngoร i thรฌ nรณ luรดn truy cแบญp ฤ‘ฦฐแปฃc, cho bแบกn mแป™t IP cแป‘ ฤ‘แป‹nh vร  vร o ฤ‘ฦฐแปฃc tแปซ bแบฅt cแปฉ ฤ‘รขu mร  khรดng cแบงn VPN hay mแปŸ cแป•ng. + +[GINERNET](https://docs.termix.site/install/ginernet) lร  nhร  tร i trแปฃ cแปงa Termix, vร  tร i liแป‡u cรณ hฦฐแป›ng dแบซn tแปซng bฦฐแป›c ฤ‘แปƒ triแปƒn khai trรชn nแปn tแบฃng VPS cแปงa hแป. + +
+ +## Dแปฏ liแป‡u sแปญ dแปฅng + +Termix gแปญi mแป™t tรญn hiแป‡u nhแป แบฉn danh mแป—i ngร y mแป™t lแบงn, ฤ‘แปƒ tรดi biแบฟt cรณ bao nhiรชu bแบฃn ฤ‘ang chแบกy vร  tรญnh nฤƒng nร o thแปฑc sแปฑ ฤ‘ฦฐแปฃc dรนng. Nรณ gแป“m mแป™t mรฃ bแบฃn cร i ngแบซu nhiรชn, sแป‘ ngฦฐแปi dรนng vร  mรกy chแปง bแบกn cรณ, phiรชn bแบฃn แปฉng dแปฅng, vร  nhแปฏng tรญnh nฤƒng (terminal, trรฌnh quแบฃn lรฝ tแป‡p, tunnel, docker, v.v.) ฤ‘รฃ dรนng trong 24 giแป qua. Nรณ khรดng bao giแป chแปฉa tรชn ngฦฐแปi dรนng, tรชn mรกy chแปง, ฤ‘แป‹a chแป‰ IP, thรดng tin ฤ‘ฤƒng nhแบญp hay bแบฅt cแปฉ thแปฉ gรฌ nhแบญn dแบกng bแบกn hoแบทc mรกy chแปง cแปงa bแบกn. + +Mแบทc ฤ‘แป‹nh lร  bแบญt. Bแบกn tแบฏt nรณ trong phแบงn Cร i ฤ‘แบทt quแบฃn trแป‹, mแปฅc Chung, hoแบทc ฤ‘แบทt `ENABLE_TELEMETRY=false` trฦฐแป›c cแบฃ khi khแปŸi ฤ‘แป™ng Termix. +
## Quyรชn gรณp -Termix lร  dแปฑ รกn miแป…n phรญ vร  mรฃ nguแป“n mแปŸ, khรดng cรณ gรณi ฤ‘ฤƒng kรฝ hay trแบฃ phรญ. Nแบฟu bแบกn thแบฅy hแปฏu รญch, hรฃy cรขn nhแบฏc quyรชn gรณp ฤ‘แปƒ giรบp trang trแบฃi chi phรญ mรกy chแปง, tรชn miแปn vร  thแปi gian phรกt triแปƒn. Cรกc khoแบฃn quyรชn gรณp cลฉng giรบp tร i trแปฃ thแปi gian nghiรชn cแปฉu vร  tรฌm hiแปƒu nhแปฏng gรฌ cแบงn thiแบฟt ฤ‘แปƒ xรขy dแปฑng cรกc tรญnh nฤƒng nhฦฐ SAML, Kubernetes vร  hแป— trแปฃ Agent. Theo dรตi tiแบฟn ฤ‘แป™ vร  quyรชn gรณp bรชn dฦฐแป›i. +Termix miแป…n phรญ vร  mรฃ nguแป“n mแปŸ, khรดng cรณ gรณi thuรช bao hay bแบฃn trแบฃ phรญ. Nแบฟu bแบกn thแบฅy hแปฏu รญch, hรฃy cรขn nhแบฏc quyรชn gรณp ฤ‘แปƒ giรบp trang trแบฃi mรกy chแปง, tรชn miแปn vร  thแปi gian phรกt triแปƒn. Quyรชn gรณp cลฉng giรบp cรณ thแปi gian tรฌm hiแปƒu nhแปฏng thแปฉ cแบงn thiแบฟt cho cรกc tรญnh nฤƒng nhฦฐ SAML, Kubernetes vร  hแป— trแปฃ agent. Theo dรตi tiแบฟn ฤ‘แป™ vร  quyรชn gรณp แปŸ bรชn dฦฐแป›i. [Quyรชn gรณp](https://donate.termix.site/)
-## Nha Tai Tro +## Nhร  tร i trแปฃ -Ban quan tam den viec dat quang cao tra phi de ho tro phat trien? Gui email toi [mail@termix.site](mailto:mail@termix.site). +Bแบกn muแป‘n ฤ‘แบทt quแบฃng cรกo trแบฃ phรญ ฤ‘แปƒ แปงng hแป™ viแป‡c phรกt triแปƒn? Gแปญi thฦฐ tแป›i [mail@termix.site](mailto:mail@termix.site).
@@ -325,10 +410,6 @@ Ban quan tam den viec dat quang cao tra phi de ho tro phat trien? Gui email toi Cloudflare     - - Tailscale - -    Akamai @@ -340,18 +421,21 @@ Ban quan tam den viec dat quang cao tra phi de ho tro phat trien? Gui email toi Rack Genius - +    + + Ginernet +

-## Ho Tro +## Hแป— trแปฃ -Neu ban can tro giup hoac muon yeu cau tinh nang voi Termix, hay truy cap trang [Van De](https://github.com/Termix-SSH/Support/issues), dang nhap va nhan `New Issue`. Vui long mo ta van de cang chi tiet cang tot, uu tien viet bang tieng Anh. Ban cung co the tham gia may chu [Discord](https://discord.gg/jVQGdvHDrf) va truy cap kenh ho tro, tuy nhien thoi gian phan hoi co the lau hon. +Cแบงn giรบp ฤ‘แปก hoแบทc muแป‘n ฤ‘แป xuแบฅt tรญnh nฤƒng? Hรฃy mแปŸ mแป™t [issue mแป›i](https://github.com/Termix-SSH/Support/issues) vร  mรด tแบฃ cร ng chi tiแบฟt cร ng tแป‘t, bแบฑng tiแบฟng Anh nแบฟu ฤ‘ฦฐแปฃc. Bแบกn cลฉng cรณ thแปƒ hแปi trong kรชnh hแป— trแปฃ trรชn [Discord](https://discord.gg/jVQGdvHDrf), tuy nhiรชn แปŸ ฤ‘รณ cรณ thแปƒ lรขu ฤ‘ฦฐแปฃc trแบฃ lแปi hฦกn.
-## Anh Chup Man Hinh +## แบขnh chแปฅp mร n hรฌnh
@@ -359,7 +443,7 @@ Neu ban can tro giup hoac muon yeu cau tinh nang voi Termix, hay truy cap trang [![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) -Xem tong quan cap nhat tren YouTube +Xem giแป›i thiแป‡u cรกc bแบฃn cแบญp nhแบญt trรชn YouTube

@@ -399,18 +483,18 @@ Neu ban can tro giup hoac muon yeu cau tinh nang voi Termix, hay truy cap trang
Nen tangPhan phoiNแปn tแบฃngBแบฃn phรขn phแป‘i
WebBat ky trinh duyet hien dai nao (Chrome, Safari, Firefox) ยท Ho tro PWAMแปi trรฌnh duyแป‡t hiแป‡n ฤ‘แบกi (Chrome, Safari, Firefox) ยท Hแป— trแปฃ PWA
Windows x64/ia32Portable ยท MSI Installer ยท ChocolateyBแบฃn chแบกy ngay ยท Bแป™ cร i MSI ยท Chocolatey
Linux x64/ia32Portable ยท AUR ยท AppImage ยท Deb ยท FlatpakBแบฃn chแบกy ngay ยท AUR ยท AppImage ยท Deb ยท Flatpak
macOS x64/ia32, v12.0+
-Mot so video va hinh anh co the da loi thoi hoac khong the hien chinh xac hoan toan cac tinh nang. +Mแป™t sแป‘ video vร  hรฌnh แบฃnh cรณ thแปƒ ฤ‘รฃ cลฉ hoแบทc chฦฐa thแปƒ hiแป‡n ฤ‘แบงy ฤ‘แปง tรญnh nฤƒng.
-## Tinh Nang Du Kien +## Tรญnh nฤƒng dแปฑ kiแบฟn -Xem [Du An](https://github.com/orgs/Termix-SSH/projects/5) de biet tat ca cac tinh nang du kien. Neu ban muon dong gop, xem [Dong Gop](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +Toร n bแป™ tรญnh nฤƒng dแปฑ kiแบฟn nแบฑm แปŸ [Projects](https://github.com/orgs/Termix-SSH/projects/5). Nแบฟu bแบกn muแป‘n ฤ‘รณng gรณp, xem [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
-## Giay Phep +## Giแบฅy phรฉp -Duoc phan phoi theo Giay Phep Apache Phien Ban 2.0. Xem `LICENSE` de biet them thong tin. +Phรกt hร nh theo Giแบฅy phรฉp Apache phiรชn bแบฃn 2.0. Xem `LICENSE` ฤ‘แปƒ biแบฟt thรชm chi tiแบฟt. diff --git a/drizzle.config.mysql.ts b/drizzle.config.mysql.ts new file mode 100644 index 0000000..5665bf7 --- /dev/null +++ b/drizzle.config.mysql.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "mysql", + schema: "./src/backend/database/db/schema.mysql.ts", + out: "./drizzle/mysql", +}); diff --git a/drizzle.config.pg.ts b/drizzle.config.pg.ts new file mode 100644 index 0000000..9129234 --- /dev/null +++ b/drizzle.config.pg.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", + schema: "./src/backend/database/db/schema.pg.ts", + out: "./drizzle/postgres", +}); diff --git a/drizzle.config.sqlite.ts b/drizzle.config.sqlite.ts new file mode 100644 index 0000000..7941897 --- /dev/null +++ b/drizzle.config.sqlite.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "sqlite", + schema: "./src/backend/database/db/schema.ts", + out: "./drizzle/sqlite", +}); diff --git a/drizzle/mysql/0000_clean_pretty_boy.sql b/drizzle/mysql/0000_clean_pretty_boy.sql new file mode 100644 index 0000000..d138297 --- /dev/null +++ b/drizzle/mysql/0000_clean_pretty_boy.sql @@ -0,0 +1,890 @@ +CREATE TABLE `alert_firings` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `rule_id` int NOT NULL, + `host_id` int NOT NULL, + `host_name` text NOT NULL, + `fired_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `resolved_at` text, + `value` double, + `message` text NOT NULL, + `severity` text NOT NULL DEFAULT ('warning'), + `acknowledged` boolean NOT NULL DEFAULT false, + CONSTRAINT `alert_firings_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `alert_rule_channels` ( + `id` int AUTO_INCREMENT NOT NULL, + `rule_id` int NOT NULL, + `channel_id` int NOT NULL, + CONSTRAINT `alert_rule_channels_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `alert_rules` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int, + `name` varchar(255) NOT NULL, + `enabled` boolean NOT NULL DEFAULT true, + `trigger_type` text NOT NULL, + `threshold_value` double, + `threshold_duration_seconds` int, + `cooldown_minutes` int NOT NULL DEFAULT 15, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `alert_rules_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `api_keys` ( + `id` varchar(255) NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `token_hash` text NOT NULL, + `token_prefix` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` text, + `last_used_at` text, + `is_active` boolean NOT NULL DEFAULT true, + CONSTRAINT `api_keys_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `audit_logs` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255), + `username` text NOT NULL, + `action` text NOT NULL, + `resource_type` text NOT NULL, + `resource_id` text, + `resource_name` text, + `details` text, + `ip_address` text, + `user_agent` text, + `success` boolean NOT NULL, + `error_message` text, + `timestamp` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `audit_logs_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `c2s_tunnel_presets` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `config` text NOT NULL, + `platform` text, + `computer_name` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `c2s_tunnel_presets_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `command_history` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `command` text NOT NULL, + `executed_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `command_history_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `dashboard_service_links` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `label` text NOT NULL, + `url` text NOT NULL, + `order` int NOT NULL DEFAULT 0, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `dashboard_service_links_id` PRIMARY KEY(`id`), + CONSTRAINT `dashboard_service_links_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `dismissed_alerts` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `alert_id` text NOT NULL, + `dismissed_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `dismissed_alerts_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `file_manager_pinned` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `name` varchar(255) NOT NULL, + `path` text NOT NULL, + `pinned_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `file_manager_pinned_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `file_manager_recent` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `name` varchar(255) NOT NULL, + `path` text NOT NULL, + `last_opened` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `file_manager_recent_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `file_manager_shortcuts` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `name` varchar(255) NOT NULL, + `path` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `file_manager_shortcuts_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `homepage_items` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `type_id` text NOT NULL, + `title` text, + `config` text NOT NULL DEFAULT ('{}'), + `folder_id` int, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `homepage_items_id` PRIMARY KEY(`id`), + CONSTRAINT `homepage_items_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `homepage_layouts` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `layout` text NOT NULL DEFAULT ('{}'), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `homepage_layouts_id` PRIMARY KEY(`id`), + CONSTRAINT `homepage_layouts_user_id_unique` UNIQUE(`user_id`) +); +--> statement-breakpoint +CREATE TABLE `host_access` ( + `id` int AUTO_INCREMENT NOT NULL, + `host_id` int NOT NULL, + `user_id` varchar(255), + `role_id` int, + `granted_by` varchar(255) NOT NULL, + `permission_level` text NOT NULL DEFAULT ('connect'), + `expires_at` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `last_accessed_at` text, + `access_count` int NOT NULL DEFAULT 0, + CONSTRAINT `host_access_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `host_health_checks` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `checks` text NOT NULL, + `interval_seconds` int NOT NULL DEFAULT 300, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `host_health_checks_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_host_health_checks_user_host` UNIQUE(`user_id`,`host_id`) +); +--> statement-breakpoint +CREATE TABLE `host_health_history` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `check_id` text NOT NULL, + `ts` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `ok` boolean NOT NULL, + `latency_ms` int, + `detail` text, + CONSTRAINT `host_health_history_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `host_metrics_history` ( + `id` int AUTO_INCREMENT NOT NULL, + `host_id` int NOT NULL, + `ts` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `cpu_percent` double, + `mem_percent` double, + `disk_percent` double, + `net_rx_bytes` int, + `net_tx_bytes` int, + CONSTRAINT `host_metrics_history_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `host_metrics_preferences` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `layout` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `host_metrics_preferences_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_host_metrics_prefs_user_host` UNIQUE(`user_id`,`host_id`) +); +--> statement-breakpoint +CREATE TABLE `ssh_data` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `connection_type` text NOT NULL DEFAULT ('ssh'), + `name` varchar(255), + `ip` text NOT NULL, + `port` int NOT NULL, + `username` text NOT NULL, + `folder` text, + `tags` text, + `pin` boolean NOT NULL DEFAULT false, + `auth_type` text NOT NULL, + `use_warpgate` boolean NOT NULL DEFAULT false, + `share_ssh_auth` boolean NOT NULL DEFAULT false, + `force_keyboard_interactive` text, + `password` text, + `key` text, + `key_password` text, + `key_type` text, + `sudo_password` text, + `autostart_password` text, + `autostart_key` text, + `autostart_key_password` text, + `credential_id` int, + `override_credential_username` boolean, + `vault_profile_id` int, + `enable_terminal` boolean NOT NULL DEFAULT true, + `enable_session_logging` boolean NOT NULL DEFAULT true, + `allow_session_sharing` boolean NOT NULL DEFAULT true, + `enable_command_history` boolean NOT NULL DEFAULT true, + `enable_tunnel` boolean NOT NULL DEFAULT true, + `tunnel_connections` text, + `jump_hosts` text, + `enable_file_manager` boolean NOT NULL DEFAULT true, + `scp_legacy` boolean NOT NULL DEFAULT false, + `enable_docker` boolean NOT NULL DEFAULT false, + `enable_tmux_monitor` boolean NOT NULL DEFAULT false, + `show_terminal_in_sidebar` boolean NOT NULL DEFAULT true, + `show_file_manager_in_sidebar` boolean NOT NULL DEFAULT false, + `show_tunnel_in_sidebar` boolean NOT NULL DEFAULT false, + `show_docker_in_sidebar` boolean NOT NULL DEFAULT false, + `show_server_stats_in_sidebar` boolean NOT NULL DEFAULT false, + `default_path` text, + `stats_config` text, + `docker_config` text, + `enable_proxmox` boolean NOT NULL DEFAULT false, + `proxmox_config` text, + `terminal_config` text, + `quick_actions` text, + `notes` text, + `enable_ssh` boolean NOT NULL DEFAULT true, + `enable_rdp` boolean NOT NULL DEFAULT false, + `enable_vnc` boolean NOT NULL DEFAULT false, + `enable_telnet` boolean NOT NULL DEFAULT false, + `ssh_port` int DEFAULT 22, + `rdp_port` int DEFAULT 3389, + `vnc_port` int DEFAULT 5900, + `telnet_port` int DEFAULT 23, + `rdp_credential_id` int, + `rdp_user` text, + `rdp_password` text, + `rdp_domain` text, + `rdp_security` text, + `rdp_ignore_cert` boolean DEFAULT false, + `vnc_credential_id` int, + `vnc_password` text, + `vnc_user` text, + `telnet_user` text, + `telnet_password` text, + `telnet_credential_id` int, + `rdp_auth_type` text, + `vnc_auth_type` text, + `telnet_auth_type` text, + `domain` text, + `security` text, + `ignore_cert` boolean DEFAULT false, + `guacamole_config` text, + `use_socks5` boolean, + `socks5_host` text, + `socks5_port` int, + `socks5_username` text, + `socks5_password` text, + `socks5_proxy_chain` text, + `connection_origin` text, + `mac_address` text, + `wol_broadcast_address` text, + `port_knock_sequence` text, + `host_key_fingerprint` text, + `host_key_type` text, + `host_key_algorithm` text DEFAULT ('sha256'), + `host_key_first_seen` text, + `host_key_last_verified` text, + `host_key_changed_count` int DEFAULT 0, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ssh_data_id` PRIMARY KEY(`id`), + CONSTRAINT `ssh_data_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `network_topology` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `topology` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `network_topology_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `notification_channels` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `type` text NOT NULL, + `config` text NOT NULL, + `enabled` boolean NOT NULL DEFAULT true, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `notification_channels_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `opkssh_tokens` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `ssh_cert` text NOT NULL, + `private_key` text NOT NULL, + `email` text, + `sub` text, + `issuer` text, + `audience` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` text NOT NULL, + `last_used` text, + CONSTRAINT `opkssh_tokens_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_opkssh_tokens_user_host` UNIQUE(`user_id`,`host_id`) +); +--> statement-breakpoint +CREATE TABLE `recent_activity` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `type` text NOT NULL, + `host_id` int NOT NULL, + `host_name` text, + `timestamp` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `recent_activity_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `roles` ( + `id` int AUTO_INCREMENT NOT NULL, + `name` varchar(255) NOT NULL, + `display_name` text NOT NULL, + `description` text, + `is_system` boolean NOT NULL DEFAULT false, + `permissions` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `roles_id` PRIMARY KEY(`id`), + CONSTRAINT `roles_name_unique` UNIQUE(`name`) +); +--> statement-breakpoint +CREATE TABLE `session_recordings` ( + `id` int AUTO_INCREMENT NOT NULL, + `host_id` int NOT NULL, + `user_id` varchar(255), + `username` text, + `access_id` int, + `started_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `ended_at` text, + `duration` int, + `commands` text, + `dangerous_actions` text, + `recording_path` text, + `protocol` varchar(255) NOT NULL DEFAULT 'ssh', + `format` text NOT NULL DEFAULT ('text'), + `terminated_by_owner` boolean DEFAULT false, + `termination_reason` text, + CONSTRAINT `session_recordings_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `session_share_participants` ( + `id` int AUTO_INCREMENT NOT NULL, + `share_id` varchar(255) NOT NULL, + `user_id` varchar(255), + `guest_label` text, + `joined_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `left_at` text, + CONSTRAINT `session_share_participants_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `session_shares` ( + `id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `owner_user_id` varchar(255) NOT NULL, + `protocol` varchar(255) NOT NULL, + `session_id` text NOT NULL, + `tab_instance_id` text, + `share_type` text NOT NULL, + `target_user_id` varchar(255), + `link_token` varchar(255), + `permission_level` text NOT NULL DEFAULT ('read-only'), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` text NOT NULL, + `revoked_at` text, + `last_joined_at` text, + `join_count` int NOT NULL DEFAULT 0, + CONSTRAINT `session_shares_id` PRIMARY KEY(`id`), + CONSTRAINT `session_shares_link_token_unique` UNIQUE(`link_token`) +); +--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` varchar(255) NOT NULL, + `user_id` varchar(255) NOT NULL, + `jwt_token` text NOT NULL, + `device_type` text NOT NULL, + `device_info` text NOT NULL, + `oidc_sub` text, + `oidc_sid` text, + `sso_provider_id` int, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` text NOT NULL, + `last_active_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `sessions_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `settings` ( + `key` varchar(255) NOT NULL, + `value` text NOT NULL, + CONSTRAINT `settings_key` PRIMARY KEY(`key`) +); +--> statement-breakpoint +CREATE TABLE `shared_host_auth_overrides` ( + `id` int AUTO_INCREMENT NOT NULL, + `host_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `protocol` varchar(255) NOT NULL DEFAULT 'ssh', + `credential_id` int NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `shared_host_auth_overrides_id` PRIMARY KEY(`id`), + CONSTRAINT `shared_host_auth_overrides_host_user_protocol_unique` UNIQUE(`host_id`,`user_id`,`protocol`) +); +--> statement-breakpoint +CREATE TABLE `shared_host_secrets` ( + `id` int AUTO_INCREMENT NOT NULL, + `host_access_id` int NOT NULL, + `target_user_id` varchar(255) NOT NULL, + `protocol` varchar(255) NOT NULL DEFAULT 'ssh', + `source_type` text NOT NULL DEFAULT ('credential'), + `original_credential_id` int, + `encrypted_username` text, + `encrypted_auth_type` text, + `encrypted_password` text, + `encrypted_key` text, + `encrypted_key_password` text, + `encrypted_key_type` text, + `encrypted_domain` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `shared_host_secrets_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_shared_host_secrets_scope` UNIQUE(`host_access_id`,`target_user_id`,`protocol`) +); +--> statement-breakpoint +CREATE TABLE `snippet_access` ( + `id` int AUTO_INCREMENT NOT NULL, + `snippet_id` int NOT NULL, + `user_id` varchar(255), + `role_id` int, + `granted_by` varchar(255) NOT NULL, + `permission_level` text NOT NULL DEFAULT ('view'), + `expires_at` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `snippet_access_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `snippet_folders` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `color` text, + `icon` text, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `snippet_folders_id` PRIMARY KEY(`id`), + CONSTRAINT `snippet_folders_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `snippets` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `content` text NOT NULL, + `description` text, + `folder` text, + `order` int NOT NULL DEFAULT 0, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `host_filter` text, + CONSTRAINT `snippets_id` PRIMARY KEY(`id`), + CONSTRAINT `snippets_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `ssh_credential_usage` ( + `id` int AUTO_INCREMENT NOT NULL, + `credential_id` int NOT NULL, + `host_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `used_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ssh_credential_usage_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `ssh_credentials` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `description` text, + `folder` text, + `tags` text, + `auth_type` text NOT NULL, + `username` text, + `password` text, + `key` text, + `private_key` text, + `public_key` text, + `key_password` text, + `key_type` text, + `detected_key_type` text, + `cert_public_key` text, + `usage_count` int NOT NULL DEFAULT 0, + `last_used` text, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ssh_credentials_id` PRIMARY KEY(`id`), + CONSTRAINT `ssh_credentials_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `ssh_folders` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `color` text, + `icon` text, + `credential_id` int, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ssh_folders_id` PRIMARY KEY(`id`), + CONSTRAINT `ssh_folders_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `sso_providers` ( + `id` int AUTO_INCREMENT NOT NULL, + `name` varchar(255) NOT NULL, + `type` text NOT NULL, + `enabled` boolean NOT NULL DEFAULT true, + `display_order` int NOT NULL DEFAULT 0, + `config` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `sso_providers_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `sync_tombstones` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `entity_type` text NOT NULL, + `sync_id` varchar(255) NOT NULL, + `deleted_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `sync_tombstones_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `termix_identities` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `handle` varchar(255) NOT NULL, + `description` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `termix_identities_id` PRIMARY KEY(`id`), + CONSTRAINT `termix_identities_user_id_unique` UNIQUE(`user_id`), + CONSTRAINT `termix_identities_handle_unique` UNIQUE(`handle`) +); +--> statement-breakpoint +CREATE TABLE `termix_identity_ca` ( + `id` int AUTO_INCREMENT NOT NULL, + `identity_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `public_key` text NOT NULL, + `private_key` text NOT NULL, + `validity_days` int NOT NULL DEFAULT 90, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `termix_identity_ca_id` PRIMARY KEY(`id`), + CONSTRAINT `termix_identity_ca_identity_id_unique` UNIQUE(`identity_id`) +); +--> statement-breakpoint +CREATE TABLE `termix_identity_keys` ( + `id` int AUTO_INCREMENT NOT NULL, + `identity_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `public_key` text NOT NULL, + `key_type` text NOT NULL, + `algorithm` text NOT NULL, + `label` text, + `comment` text, + `source` text NOT NULL DEFAULT ('manual'), + `credential_id` int, + `enabled` boolean NOT NULL DEFAULT true, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `termix_identity_keys_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `tmux_session_tags` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `session_name` text NOT NULL, + `tag` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `tmux_session_tags_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `transfer_recent` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `source_host_id` int NOT NULL, + `dest_host_id` int NOT NULL, + `dest_path` text NOT NULL, + `dest_path_label` text NOT NULL, + `last_used` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `transfer_recent_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `trusted_devices` ( + `id` varchar(255) NOT NULL, + `user_id` varchar(255) NOT NULL, + `device_fingerprint` text NOT NULL, + `device_type` text NOT NULL, + `device_info` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` text NOT NULL, + `last_used_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `trusted_devices_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `user_open_tabs` ( + `id` varchar(255) NOT NULL, + `user_id` varchar(255) NOT NULL, + `tab_type` text NOT NULL, + `host_id` int, + `label` text NOT NULL, + `tab_order` int NOT NULL DEFAULT 0, + `backend_session_id` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `user_open_tabs_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `user_preferences` ( + `user_id` varchar(255) NOT NULL, + `reopen_tabs_on_login` boolean NOT NULL DEFAULT false, + `theme` text, + `font_size` text, + `accent_color` text, + `language` text, + `storage_mode` text, + `command_autocomplete` boolean, + `command_palette_enabled` boolean, + `show_host_tags` boolean, + `host_tray_on_click` boolean, + `pin_app_rail` boolean, + `expand_app_rail_on_hover` boolean, + `folders_collapsed` boolean, + `confirm_snippet_execution` boolean, + `disable_update_check` boolean, + `confirm_tab_close` boolean, + `hidden_rail_tabs` text, + `compact_host_view` boolean, + `status_color_scheme` text, + `custom_themes` text, + `custom_keybindings` text, + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `user_preferences_user_id` PRIMARY KEY(`user_id`) +); +--> statement-breakpoint +CREATE TABLE `user_roles` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `role_id` int NOT NULL, + `granted_by` varchar(255), + `granted_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `user_roles_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_user_roles_user_role` UNIQUE(`user_id`,`role_id`) +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` varchar(255) NOT NULL, + `username` text NOT NULL, + `password_hash` text NOT NULL, + `is_admin` boolean NOT NULL DEFAULT false, + `is_oidc` boolean NOT NULL DEFAULT false, + `oidc_identifier` text, + `sso_provider_id` int, + `client_id` text, + `client_secret` text, + `issuer_url` text, + `authorization_url` text, + `token_url` text, + `identifier_path` text, + `name_path` text, + `scopes` text DEFAULT ('openid email profile'), + `totp_secret` text, + `totp_enabled` boolean NOT NULL DEFAULT false, + `totp_backup_codes` text, + `registered_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `donation_modal_dismissed` boolean NOT NULL DEFAULT false, + CONSTRAINT `users_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `vault_profiles` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `description` text, + `folder` text, + `tags` text, + `vault_addr` text NOT NULL, + `vault_namespace` text, + `oidc_mount` text, + `oidc_role` text, + `ssh_mount` text, + `ssh_role` text NOT NULL, + `valid_principals` text, + `key_type` text, + `shared` boolean NOT NULL DEFAULT false, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `vault_profiles_id` PRIMARY KEY(`id`), + CONSTRAINT `vault_profiles_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +CREATE TABLE `vault_tokens` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `profile_id` int NOT NULL, + `ssh_cert` text NOT NULL, + `private_key` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` text NOT NULL, + `last_used` text, + CONSTRAINT `vault_tokens_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_vault_tokens_user_profile` UNIQUE(`user_id`,`profile_id`) +); +--> statement-breakpoint +CREATE TABLE `webauthn_credentials` ( + `id` varchar(255) NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `credential_id` text NOT NULL, + `public_key` text NOT NULL, + `counter` int NOT NULL DEFAULT 0, + `device_type` text, + `backed_up` boolean NOT NULL DEFAULT false, + `transports` text, + `user_verification` text NOT NULL DEFAULT ('preferred'), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `last_used_at` text, + CONSTRAINT `webauthn_credentials_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +ALTER TABLE `alert_firings` ADD CONSTRAINT `alert_firings_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `alert_firings` ADD CONSTRAINT `alert_firings_rule_id_alert_rules_id_fk` FOREIGN KEY (`rule_id`) REFERENCES `alert_rules`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `alert_rule_channels` ADD CONSTRAINT `alert_rule_channels_rule_id_alert_rules_id_fk` FOREIGN KEY (`rule_id`) REFERENCES `alert_rules`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `alert_rule_channels` ADD CONSTRAINT `alert_rule_channels_channel_id_notification_channels_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `notification_channels`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `alert_rules` ADD CONSTRAINT `alert_rules_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `alert_rules` ADD CONSTRAINT `alert_rules_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `api_keys` ADD CONSTRAINT `api_keys_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `audit_logs` ADD CONSTRAINT `audit_logs_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `c2s_tunnel_presets` ADD CONSTRAINT `c2s_tunnel_presets_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `command_history` ADD CONSTRAINT `command_history_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `command_history` ADD CONSTRAINT `command_history_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `dashboard_service_links` ADD CONSTRAINT `dashboard_service_links_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `dismissed_alerts` ADD CONSTRAINT `dismissed_alerts_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `file_manager_pinned` ADD CONSTRAINT `file_manager_pinned_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `file_manager_pinned` ADD CONSTRAINT `file_manager_pinned_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `file_manager_recent` ADD CONSTRAINT `file_manager_recent_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `file_manager_recent` ADD CONSTRAINT `file_manager_recent_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `file_manager_shortcuts` ADD CONSTRAINT `file_manager_shortcuts_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `file_manager_shortcuts` ADD CONSTRAINT `file_manager_shortcuts_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `homepage_items` ADD CONSTRAINT `homepage_items_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `homepage_layouts` ADD CONSTRAINT `homepage_layouts_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_access` ADD CONSTRAINT `host_access_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_access` ADD CONSTRAINT `host_access_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_access` ADD CONSTRAINT `host_access_role_id_roles_id_fk` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_access` ADD CONSTRAINT `host_access_granted_by_users_id_fk` FOREIGN KEY (`granted_by`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_health_checks` ADD CONSTRAINT `host_health_checks_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_health_checks` ADD CONSTRAINT `host_health_checks_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_health_history` ADD CONSTRAINT `host_health_history_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_health_history` ADD CONSTRAINT `host_health_history_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_metrics_history` ADD CONSTRAINT `host_metrics_history_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_metrics_preferences` ADD CONSTRAINT `host_metrics_preferences_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `host_metrics_preferences` ADD CONSTRAINT `host_metrics_preferences_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD CONSTRAINT `ssh_data_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD CONSTRAINT `ssh_data_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD CONSTRAINT `ssh_data_vault_profile_id_vault_profiles_id_fk` FOREIGN KEY (`vault_profile_id`) REFERENCES `vault_profiles`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD CONSTRAINT `ssh_data_rdp_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`rdp_credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD CONSTRAINT `ssh_data_vnc_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`vnc_credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD CONSTRAINT `ssh_data_telnet_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`telnet_credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `network_topology` ADD CONSTRAINT `network_topology_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `notification_channels` ADD CONSTRAINT `notification_channels_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `opkssh_tokens` ADD CONSTRAINT `opkssh_tokens_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `opkssh_tokens` ADD CONSTRAINT `opkssh_tokens_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `recent_activity` ADD CONSTRAINT `recent_activity_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `recent_activity` ADD CONSTRAINT `recent_activity_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_recordings` ADD CONSTRAINT `session_recordings_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_recordings` ADD CONSTRAINT `session_recordings_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_recordings` ADD CONSTRAINT `session_recordings_access_id_host_access_id_fk` FOREIGN KEY (`access_id`) REFERENCES `host_access`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_share_participants` ADD CONSTRAINT `session_share_participants_share_id_session_shares_id_fk` FOREIGN KEY (`share_id`) REFERENCES `session_shares`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_share_participants` ADD CONSTRAINT `session_share_participants_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_shares` ADD CONSTRAINT `session_shares_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_shares` ADD CONSTRAINT `session_shares_owner_user_id_users_id_fk` FOREIGN KEY (`owner_user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `session_shares` ADD CONSTRAINT `session_shares_target_user_id_users_id_fk` FOREIGN KEY (`target_user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `sessions` ADD CONSTRAINT `sessions_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `shared_host_auth_overrides` ADD CONSTRAINT `shared_host_auth_overrides_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `shared_host_auth_overrides` ADD CONSTRAINT `shared_host_auth_overrides_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `shared_host_auth_overrides` ADD CONSTRAINT `shared_host_auth_overrides_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `shared_host_secrets` ADD CONSTRAINT `shared_host_secrets_host_access_id_host_access_id_fk` FOREIGN KEY (`host_access_id`) REFERENCES `host_access`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `shared_host_secrets` ADD CONSTRAINT `shared_host_secrets_target_user_id_users_id_fk` FOREIGN KEY (`target_user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `shared_host_secrets` ADD CONSTRAINT `shared_host_secrets_original_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`original_credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `snippet_access` ADD CONSTRAINT `snippet_access_snippet_id_snippets_id_fk` FOREIGN KEY (`snippet_id`) REFERENCES `snippets`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `snippet_access` ADD CONSTRAINT `snippet_access_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `snippet_access` ADD CONSTRAINT `snippet_access_role_id_roles_id_fk` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `snippet_access` ADD CONSTRAINT `snippet_access_granted_by_users_id_fk` FOREIGN KEY (`granted_by`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `snippet_folders` ADD CONSTRAINT `snippet_folders_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `snippets` ADD CONSTRAINT `snippets_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_credential_usage` ADD CONSTRAINT `ssh_credential_usage_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_credential_usage` ADD CONSTRAINT `ssh_credential_usage_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_credential_usage` ADD CONSTRAINT `ssh_credential_usage_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_credentials` ADD CONSTRAINT `ssh_credentials_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_folders` ADD CONSTRAINT `ssh_folders_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ssh_folders` ADD CONSTRAINT `ssh_folders_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `sync_tombstones` ADD CONSTRAINT `sync_tombstones_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `termix_identities` ADD CONSTRAINT `termix_identities_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `termix_identity_ca` ADD CONSTRAINT `termix_identity_ca_identity_id_termix_identities_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `termix_identities`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `termix_identity_ca` ADD CONSTRAINT `termix_identity_ca_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `termix_identity_keys` ADD CONSTRAINT `termix_identity_keys_identity_id_termix_identities_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `termix_identities`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `termix_identity_keys` ADD CONSTRAINT `termix_identity_keys_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `termix_identity_keys` ADD CONSTRAINT `termix_identity_keys_credential_id_ssh_credentials_id_fk` FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `tmux_session_tags` ADD CONSTRAINT `tmux_session_tags_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `tmux_session_tags` ADD CONSTRAINT `tmux_session_tags_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `transfer_recent` ADD CONSTRAINT `transfer_recent_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `transfer_recent` ADD CONSTRAINT `transfer_recent_source_host_id_ssh_data_id_fk` FOREIGN KEY (`source_host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `transfer_recent` ADD CONSTRAINT `transfer_recent_dest_host_id_ssh_data_id_fk` FOREIGN KEY (`dest_host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `trusted_devices` ADD CONSTRAINT `trusted_devices_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `user_open_tabs` ADD CONSTRAINT `user_open_tabs_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `user_open_tabs` ADD CONSTRAINT `user_open_tabs_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `user_preferences` ADD CONSTRAINT `user_preferences_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `user_roles` ADD CONSTRAINT `user_roles_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `user_roles` ADD CONSTRAINT `user_roles_role_id_roles_id_fk` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `user_roles` ADD CONSTRAINT `user_roles_granted_by_users_id_fk` FOREIGN KEY (`granted_by`) REFERENCES `users`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `vault_profiles` ADD CONSTRAINT `vault_profiles_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `vault_tokens` ADD CONSTRAINT `vault_tokens_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `vault_tokens` ADD CONSTRAINT `vault_tokens_profile_id_vault_profiles_id_fk` FOREIGN KEY (`profile_id`) REFERENCES `vault_profiles`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `webauthn_credentials` ADD CONSTRAINT `webauthn_credentials_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0001_puzzling_aqueduct.sql b/drizzle/mysql/0001_puzzling_aqueduct.sql new file mode 100644 index 0000000..5b998a6 --- /dev/null +++ b/drizzle/mysql/0001_puzzling_aqueduct.sql @@ -0,0 +1,10 @@ +CREATE TABLE `host_sidebar_preferences` ( + `user_id` varchar(255) NOT NULL, + `data` text NOT NULL, + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `host_sidebar_preferences_user_id` PRIMARY KEY(`user_id`) +); +--> statement-breakpoint +ALTER TABLE `ssh_data` ADD `sort_order` int;--> statement-breakpoint +ALTER TABLE `ssh_folders` ADD `sort_order` int;--> statement-breakpoint +ALTER TABLE `host_sidebar_preferences` ADD CONSTRAINT `host_sidebar_preferences_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0002_bumpy_korg.sql b/drizzle/mysql/0002_bumpy_korg.sql new file mode 100644 index 0000000..29ff064 --- /dev/null +++ b/drizzle/mysql/0002_bumpy_korg.sql @@ -0,0 +1,10 @@ +CREATE TABLE `credential_sidebar_preferences` ( + `user_id` varchar(255) NOT NULL, + `data` text NOT NULL, + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `credential_sidebar_preferences_user_id` PRIMARY KEY(`user_id`) +); +--> statement-breakpoint +ALTER TABLE `ssh_credentials` ADD `pin` boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `ssh_credentials` ADD `sort_order` int;--> statement-breakpoint +ALTER TABLE `credential_sidebar_preferences` ADD CONSTRAINT `credential_sidebar_preferences_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0003_dry_human_robot.sql b/drizzle/mysql/0003_dry_human_robot.sql new file mode 100644 index 0000000..34a83b4 --- /dev/null +++ b/drizzle/mysql/0003_dry_human_robot.sql @@ -0,0 +1 @@ +ALTER TABLE `snippets` ADD `is_note` boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/mysql/0004_fancy_spencer_smythe.sql b/drizzle/mysql/0004_fancy_spencer_smythe.sql new file mode 100644 index 0000000..55dbe07 --- /dev/null +++ b/drizzle/mysql/0004_fancy_spencer_smythe.sql @@ -0,0 +1,28 @@ +CREATE TABLE `proxmox_node_history` ( + `id` int AUTO_INCREMENT NOT NULL, + `host_id` int NOT NULL, + `ts` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `cpu_percent` double, + `mem_percent` double, + `disk_percent` double, + `net_rx_bytes` int, + `net_tx_bytes` int, + CONSTRAINT `proxmox_node_history_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `proxmox_stats_preferences` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `layout` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `proxmox_stats_preferences_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_proxmox_stats_prefs_user_host` UNIQUE(`user_id`,`host_id`) +); +--> statement-breakpoint +ALTER TABLE `ssh_data` ADD `enable_proxmox_stats` boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD `proxmox_stats_config` text;--> statement-breakpoint +ALTER TABLE `proxmox_node_history` ADD CONSTRAINT `proxmox_node_history_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `proxmox_stats_preferences` ADD CONSTRAINT `proxmox_stats_preferences_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `proxmox_stats_preferences` ADD CONSTRAINT `proxmox_stats_preferences_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0005_tearful_mindworm.sql b/drizzle/mysql/0005_tearful_mindworm.sql new file mode 100644 index 0000000..a4fd898 --- /dev/null +++ b/drizzle/mysql/0005_tearful_mindworm.sql @@ -0,0 +1 @@ +ALTER TABLE `ssh_data` ADD `enable_terminal_toolbar` boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/drizzle/mysql/0006_last_fantastic_four.sql b/drizzle/mysql/0006_last_fantastic_four.sql new file mode 100644 index 0000000..153a100 --- /dev/null +++ b/drizzle/mysql/0006_last_fantastic_four.sql @@ -0,0 +1,45 @@ +CREATE TABLE `fleet_inventory` ( + `id` int AUTO_INCREMENT NOT NULL, + `host_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `os_pretty_name` text, + `kernel` text, + `architecture` text, + `hostname` text, + `uptime_seconds` int, + `ip` text, + `package_manager` text, + `collected_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `fleet_inventory_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_fleet_inventory_host` UNIQUE(`host_id`,`user_id`) +); +--> statement-breakpoint +CREATE TABLE `fleet_members` ( + `id` int AUTO_INCREMENT NOT NULL, + `fleet_id` int NOT NULL, + `host_id` int NOT NULL, + `added_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `fleet_members_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_fleet_members_fleet_host` UNIQUE(`fleet_id`,`host_id`) +); +--> statement-breakpoint +CREATE TABLE `fleets` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `description` text, + `color` text, + `icon` text, + `tag_rules` text, + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `fleets_id` PRIMARY KEY(`id`), + CONSTRAINT `fleets_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +ALTER TABLE `fleet_inventory` ADD CONSTRAINT `fleet_inventory_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `fleet_inventory` ADD CONSTRAINT `fleet_inventory_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `fleet_members` ADD CONSTRAINT `fleet_members_fleet_id_fleets_id_fk` FOREIGN KEY (`fleet_id`) REFERENCES `fleets`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `fleet_members` ADD CONSTRAINT `fleet_members_host_id_ssh_data_id_fk` FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `fleets` ADD CONSTRAINT `fleets_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0007_aspiring_turbo.sql b/drizzle/mysql/0007_aspiring_turbo.sql new file mode 100644 index 0000000..b445975 --- /dev/null +++ b/drizzle/mysql/0007_aspiring_turbo.sql @@ -0,0 +1,2 @@ +ALTER TABLE `ssh_data` ADD `parent_host_id` int;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD CONSTRAINT `ssh_data_parent_host_id_ssh_data_id_fk` FOREIGN KEY (`parent_host_id`) REFERENCES `ssh_data`(`id`) ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0008_lame_nighthawk.sql b/drizzle/mysql/0008_lame_nighthawk.sql new file mode 100644 index 0000000..28cd356 --- /dev/null +++ b/drizzle/mysql/0008_lame_nighthawk.sql @@ -0,0 +1,18 @@ +CREATE TABLE `user_workspaces` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `color` text, + `icon` text, + `kind` text NOT NULL DEFAULT ('manual'), + `is_default` boolean NOT NULL DEFAULT false, + `payload` text NOT NULL DEFAULT ('{}'), + `sync_id` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `last_used_at` text, + CONSTRAINT `user_workspaces_id` PRIMARY KEY(`id`), + CONSTRAINT `user_workspaces_sync_id_unique` UNIQUE(`sync_id`) +); +--> statement-breakpoint +ALTER TABLE `user_workspaces` ADD CONSTRAINT `user_workspaces_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0009_tan_skaar.sql b/drizzle/mysql/0009_tan_skaar.sql new file mode 100644 index 0000000..7299bbf --- /dev/null +++ b/drizzle/mysql/0009_tan_skaar.sql @@ -0,0 +1,26 @@ +ALTER TABLE `api_keys` MODIFY COLUMN `expires_at` varchar(255);--> statement-breakpoint +ALTER TABLE `audit_logs` MODIFY COLUMN `action` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `audit_logs` MODIFY COLUMN `resource_type` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `audit_logs` MODIFY COLUMN `timestamp` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `host_access` MODIFY COLUMN `expires_at` varchar(255);--> statement-breakpoint +ALTER TABLE `opkssh_tokens` MODIFY COLUMN `expires_at` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `recent_activity` MODIFY COLUMN `timestamp` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `session_shares` MODIFY COLUMN `expires_at` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `sessions` MODIFY COLUMN `expires_at` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `snippet_access` MODIFY COLUMN `expires_at` varchar(255);--> statement-breakpoint +ALTER TABLE `trusted_devices` MODIFY COLUMN `expires_at` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `vault_tokens` MODIFY COLUMN `expires_at` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `webauthn_credentials` MODIFY COLUMN `credential_id` varchar(255) NOT NULL;--> statement-breakpoint +CREATE INDEX `idx_audit_logs_timestamp` ON `audit_logs` (`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_audit_logs_user_ts` ON `audit_logs` (`user_id`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_audit_logs_action_ts` ON `audit_logs` (`action`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_audit_logs_resource_ts` ON `audit_logs` (`resource_type`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_host_access_user_id` ON `host_access` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_host_access_role_id` ON `host_access` (`role_id`);--> statement-breakpoint +CREATE INDEX `idx_host_access_host_id` ON `host_access` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_host_access_expires_at` ON `host_access` (`expires_at`);--> statement-breakpoint +CREATE INDEX `idx_ssh_data_user_id` ON `ssh_data` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_data_parent_host` ON `ssh_data` (`parent_host_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_data_credential` ON `ssh_data` (`credential_id`);--> statement-breakpoint +CREATE INDEX `idx_sessions_user_id` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_sessions_expires_at` ON `sessions` (`expires_at`); \ No newline at end of file diff --git a/drizzle/mysql/0010_small_infant_terrible.sql b/drizzle/mysql/0010_small_infant_terrible.sql new file mode 100644 index 0000000..5eca243 --- /dev/null +++ b/drizzle/mysql/0010_small_infant_terrible.sql @@ -0,0 +1,8 @@ +CREATE TABLE `ui_preferences` ( + `user_id` varchar(255) NOT NULL, + `data` text NOT NULL, + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ui_preferences_user_id` PRIMARY KEY(`user_id`) +); +--> statement-breakpoint +ALTER TABLE `ui_preferences` ADD CONSTRAINT `ui_preferences_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/mysql/0011_easy_the_order.sql b/drizzle/mysql/0011_easy_the_order.sql new file mode 100644 index 0000000..9032ffc --- /dev/null +++ b/drizzle/mysql/0011_easy_the_order.sql @@ -0,0 +1,32 @@ +ALTER TABLE `alert_firings` MODIFY COLUMN `fired_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `session_recordings` MODIFY COLUMN `started_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `session_shares` MODIFY COLUMN `session_id` varchar(255) NOT NULL;--> statement-breakpoint +CREATE INDEX `idx_alert_firings_rule` ON `alert_firings` (`rule_id`,`fired_at`);--> statement-breakpoint +CREATE INDEX `idx_alert_firings_host` ON `alert_firings` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_api_keys_user_id` ON `api_keys` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_command_history_user_host` ON `command_history` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_dismissed_alerts_user_id` ON `dismissed_alerts` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_file_manager_pinned_user` ON `file_manager_pinned` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_file_manager_recent_user` ON `file_manager_recent` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_file_manager_shortcuts_user` ON `file_manager_shortcuts` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_fleet_inventory_user` ON `fleet_inventory` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_fleet_members_host` ON `fleet_members` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_homepage_items_user_id` ON `homepage_items` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_recent_activity_user_ts` ON `recent_activity` (`user_id`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_session_recordings_user_started` ON `session_recordings` (`user_id`,`started_at`);--> statement-breakpoint +CREATE INDEX `idx_session_recordings_host` ON `session_recordings` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_session_shares_session_id` ON `session_shares` (`session_id`);--> statement-breakpoint +CREATE INDEX `idx_session_shares_host_id` ON `session_shares` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_snippet_access_user_id` ON `snippet_access` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_snippet_access_snippet_id` ON `snippet_access` (`snippet_id`);--> statement-breakpoint +CREATE INDEX `idx_snippet_access_role_id` ON `snippet_access` (`role_id`);--> statement-breakpoint +CREATE INDEX `idx_snippets_user_id` ON `snippets` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_credential_usage_credential` ON `ssh_credential_usage` (`credential_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_credential_usage_user` ON `ssh_credential_usage` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_credentials_user_id` ON `ssh_credentials` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_folders_user_id` ON `ssh_folders` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_transfer_recent_user` ON `transfer_recent` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_trusted_devices_user_id` ON `trusted_devices` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_user_open_tabs_user_id` ON `user_open_tabs` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_user_roles_role_id` ON `user_roles` (`role_id`);--> statement-breakpoint +CREATE INDEX `idx_user_workspaces_user_id` ON `user_workspaces` (`user_id`); \ No newline at end of file diff --git a/drizzle/mysql/0012_outgoing_ultron.sql b/drizzle/mysql/0012_outgoing_ultron.sql new file mode 100644 index 0000000..ec1a5fc --- /dev/null +++ b/drizzle/mysql/0012_outgoing_ultron.sql @@ -0,0 +1,224 @@ +CREATE TABLE `ai_conversations` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `title` text, + `provider_id` int, + `model` text, + `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ai_conversations_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `ai_messages` ( + `id` int AUTO_INCREMENT NOT NULL, + `conversation_id` int NOT NULL, + `role` text NOT NULL, + `content` text NOT NULL DEFAULT (''), + `tool_calls` text, + `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ai_messages_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `ai_proposals` ( + `id` int AUTO_INCREMENT NOT NULL, + `conversation_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `kind` text NOT NULL, + `summary` text, + `payload` text NOT NULL DEFAULT ('{}'), + `status` varchar(255) NOT NULL DEFAULT 'pending', + `applied_at` text, + `result_summary` text, + `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ai_proposals_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `ai_providers` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `provider_type` text NOT NULL, + `label` varchar(255) NOT NULL, + `base_url` text, + `api_key` text, + `api_key_prefix` text, + `default_model` text, + `enabled` boolean NOT NULL DEFAULT true, + `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `ai_providers_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_ai_providers_user_label` UNIQUE(`user_id`,`label`) +); +--> statement-breakpoint +CREATE TABLE `automation_channels` ( + `id` int AUTO_INCREMENT NOT NULL, + `automation_id` int NOT NULL, + `channel_id` int NOT NULL, + CONSTRAINT `automation_channels_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_automation_channels_pair` UNIQUE(`automation_id`,`channel_id`) +); +--> statement-breakpoint +CREATE TABLE `automation_run_steps` ( + `id` int AUTO_INCREMENT NOT NULL, + `run_id` int NOT NULL, + `step_index` int NOT NULL, + `step_id` text NOT NULL, + `step_type` text NOT NULL, + `status` varchar(255) NOT NULL, + `started_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `finished_at` text, + `output` text, + `error` text, + `truncated` boolean NOT NULL DEFAULT false, + CONSTRAINT `automation_run_steps_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `automation_runs` ( + `id` int AUTO_INCREMENT NOT NULL, + `automation_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `trigger_type` text NOT NULL, + `trigger_context` text, + `status` varchar(255) NOT NULL, + `started_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `finished_at` text, + `duration_ms` int, + `error` text, + `dry_run` boolean NOT NULL DEFAULT false, + `parent_run_id` int, + CONSTRAINT `automation_runs_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `automation_schedules` ( + `id` int AUTO_INCREMENT NOT NULL, + `automation_id` int NOT NULL, + `cron` text, + `interval_seconds` int, + `timezone` text, + `next_due_at` varchar(255), + `last_tick_at` text, + CONSTRAINT `automation_schedules_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_automation_schedules_automation` UNIQUE(`automation_id`) +); +--> statement-breakpoint +CREATE TABLE `automation_trigger_state` ( + `id` int AUTO_INCREMENT NOT NULL, + `automation_id` int NOT NULL, + `state_key` varchar(255) NOT NULL, + `breach_started_at` text, + `last_fired_at` text, + `last_value` double, + `last_observed_state` text, + `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `automation_trigger_state_id` PRIMARY KEY(`id`), + CONSTRAINT `idx_automation_trigger_state_key` UNIQUE(`automation_id`,`state_key`) +); +--> statement-breakpoint +CREATE TABLE `automations` ( + `id` int AUTO_INCREMENT NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `description` text, + `enabled` boolean NOT NULL DEFAULT true, + `definition` text NOT NULL, + `definition_version` int NOT NULL DEFAULT 1, + `concurrency_policy` text NOT NULL DEFAULT ('skip'), + `max_run_seconds` int NOT NULL DEFAULT 300, + `dry_run` boolean NOT NULL DEFAULT false, + `last_run_at` text, + `last_run_status` text, + `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + CONSTRAINT `automations_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +ALTER TABLE `alert_rules` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `alert_rules` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `api_keys` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `c2s_tunnel_presets` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `c2s_tunnel_presets` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `credential_sidebar_preferences` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `dashboard_service_links` MODIFY COLUMN `label` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `dashboard_service_links` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `dashboard_service_links` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `file_manager_shortcuts` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `fleets` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `fleets` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `homepage_items` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `homepage_items` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `homepage_layouts` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `host_access` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `host_health_checks` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `host_health_checks` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `host_metrics_preferences` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `host_metrics_preferences` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `host_sidebar_preferences` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `ssh_data` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `ssh_data` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `network_topology` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `network_topology` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `notification_channels` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `opkssh_tokens` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `proxmox_stats_preferences` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `proxmox_stats_preferences` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `roles` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `roles` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `session_shares` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `sessions` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `shared_host_auth_overrides` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `shared_host_auth_overrides` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `shared_host_secrets` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `shared_host_secrets` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `snippet_access` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `snippet_folders` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `snippet_folders` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `snippets` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `snippets` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `ssh_credentials` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `ssh_credentials` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `ssh_folders` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `ssh_folders` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `sso_providers` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `sso_providers` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `termix_identities` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `termix_identities` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `termix_identity_ca` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `termix_identity_ca` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `termix_identity_keys` MODIFY COLUMN `label` varchar(255);--> statement-breakpoint +ALTER TABLE `termix_identity_keys` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `tmux_session_tags` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `trusted_devices` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `ui_preferences` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `user_open_tabs` MODIFY COLUMN `label` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `user_open_tabs` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `user_open_tabs` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `user_preferences` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `user_workspaces` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `user_workspaces` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `vault_profiles` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `vault_profiles` MODIFY COLUMN `updated_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `vault_tokens` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `webauthn_credentials` MODIFY COLUMN `created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint +ALTER TABLE `user_preferences` ADD `ai_assistant_enabled` boolean;--> statement-breakpoint +ALTER TABLE `user_preferences` ADD `ai_read_only_commands` boolean;--> statement-breakpoint +ALTER TABLE `ai_conversations` ADD CONSTRAINT `ai_conversations_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ai_messages` ADD CONSTRAINT `ai_messages_conversation_id_ai_conversations_id_fk` FOREIGN KEY (`conversation_id`) REFERENCES `ai_conversations`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ai_proposals` ADD CONSTRAINT `ai_proposals_conversation_id_ai_conversations_id_fk` FOREIGN KEY (`conversation_id`) REFERENCES `ai_conversations`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ai_proposals` ADD CONSTRAINT `ai_proposals_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ai_providers` ADD CONSTRAINT `ai_providers_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automation_channels` ADD CONSTRAINT `automation_channels_automation_id_automations_id_fk` FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automation_channels` ADD CONSTRAINT `automation_channels_channel_id_notification_channels_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `notification_channels`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automation_run_steps` ADD CONSTRAINT `automation_run_steps_run_id_automation_runs_id_fk` FOREIGN KEY (`run_id`) REFERENCES `automation_runs`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automation_runs` ADD CONSTRAINT `automation_runs_automation_id_automations_id_fk` FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automation_runs` ADD CONSTRAINT `automation_runs_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automation_schedules` ADD CONSTRAINT `automation_schedules_automation_id_automations_id_fk` FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automation_trigger_state` ADD CONSTRAINT `automation_trigger_state_automation_id_automations_id_fk` FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `automations` ADD CONSTRAINT `automations_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `idx_ai_conversations_user` ON `ai_conversations` (`user_id`,`updated_at`);--> statement-breakpoint +CREATE INDEX `idx_ai_messages_conversation` ON `ai_messages` (`conversation_id`,`created_at`);--> statement-breakpoint +CREATE INDEX `idx_ai_proposals_user` ON `ai_proposals` (`user_id`,`status`);--> statement-breakpoint +CREATE INDEX `idx_ai_proposals_conversation` ON `ai_proposals` (`conversation_id`);--> statement-breakpoint +CREATE INDEX `idx_automation_run_steps_run` ON `automation_run_steps` (`run_id`,`step_index`);--> statement-breakpoint +CREATE INDEX `idx_automation_runs_automation` ON `automation_runs` (`automation_id`,`started_at`);--> statement-breakpoint +CREATE INDEX `idx_automation_runs_user` ON `automation_runs` (`user_id`,`started_at`);--> statement-breakpoint +CREATE INDEX `idx_automation_schedules_due` ON `automation_schedules` (`next_due_at`);--> statement-breakpoint +CREATE INDEX `idx_automations_user` ON `automations` (`user_id`,`enabled`); \ No newline at end of file diff --git a/drizzle/mysql/0013_third_wraith.sql b/drizzle/mysql/0013_third_wraith.sql new file mode 100644 index 0000000..0ebb111 --- /dev/null +++ b/drizzle/mysql/0013_third_wraith.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user_preferences` ADD `terminal_defaults` text;--> statement-breakpoint +ALTER TABLE `user_preferences` ADD `rdp_defaults` text; \ No newline at end of file diff --git a/drizzle/mysql/0014_bitter_nextwave.sql b/drizzle/mysql/0014_bitter_nextwave.sql new file mode 100644 index 0000000..df84506 --- /dev/null +++ b/drizzle/mysql/0014_bitter_nextwave.sql @@ -0,0 +1 @@ +ALTER TABLE `user_preferences` ADD `terminal_macros` text; \ No newline at end of file diff --git a/drizzle/mysql/meta/0000_snapshot.json b/drizzle/mysql/meta/0000_snapshot.json new file mode 100644 index 0000000..4c6f31c --- /dev/null +++ b/drizzle/mysql/meta/0000_snapshot.json @@ -0,0 +1,6437 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "2b238de4-3ad7-4b57-8c58-308d6da06c02", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0001_snapshot.json b/drizzle/mysql/meta/0001_snapshot.json new file mode 100644 index 0000000..2ae914f --- /dev/null +++ b/drizzle/mysql/meta/0001_snapshot.json @@ -0,0 +1,6504 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "c90a5027-6791-426e-a610-1bcdd57a3d82", + "prevId": "2b238de4-3ad7-4b57-8c58-308d6da06c02", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0002_snapshot.json b/drizzle/mysql/meta/0002_snapshot.json new file mode 100644 index 0000000..eda963b --- /dev/null +++ b/drizzle/mysql/meta/0002_snapshot.json @@ -0,0 +1,6572 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "2fafb1cd-cb3f-4655-9c35-9699934e7ad4", + "prevId": "c90a5027-6791-426e-a610-1bcdd57a3d82", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0003_snapshot.json b/drizzle/mysql/meta/0003_snapshot.json new file mode 100644 index 0000000..cd24d70 --- /dev/null +++ b/drizzle/mysql/meta/0003_snapshot.json @@ -0,0 +1,6580 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "3b19c9ab-23e3-47e2-a8e4-ede347f0ad40", + "prevId": "2fafb1cd-cb3f-4655-9c35-9699934e7ad4", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0004_snapshot.json b/drizzle/mysql/meta/0004_snapshot.json new file mode 100644 index 0000000..b1cd8c3 --- /dev/null +++ b/drizzle/mysql/meta/0004_snapshot.json @@ -0,0 +1,6780 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "608b427f-8231-4295-b471-e62b484c0211", + "prevId": "3b19c9ab-23e3-47e2-a8e4-ede347f0ad40", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0005_snapshot.json b/drizzle/mysql/meta/0005_snapshot.json new file mode 100644 index 0000000..c9c5b79 --- /dev/null +++ b/drizzle/mysql/meta/0005_snapshot.json @@ -0,0 +1,6788 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "97c005f3-801f-48dc-9788-58bfb7da1c54", + "prevId": "608b427f-8231-4295-b471-e62b484c0211", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0006_snapshot.json b/drizzle/mysql/meta/0006_snapshot.json new file mode 100644 index 0000000..b3df268 --- /dev/null +++ b/drizzle/mysql/meta/0006_snapshot.json @@ -0,0 +1,7111 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "e7cde432-1ea5-4654-9bd8-aaaf78627203", + "prevId": "97c005f3-801f-48dc-9788-58bfb7da1c54", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0007_snapshot.json b/drizzle/mysql/meta/0007_snapshot.json new file mode 100644 index 0000000..b4571dc --- /dev/null +++ b/drizzle/mysql/meta/0007_snapshot.json @@ -0,0 +1,7131 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "8f6d860f-ee22-4e2e-b913-25cec9faf4e9", + "prevId": "e7cde432-1ea5-4654-9bd8-aaaf78627203", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0008_snapshot.json b/drizzle/mysql/meta/0008_snapshot.json new file mode 100644 index 0000000..38a6a4f --- /dev/null +++ b/drizzle/mysql/meta/0008_snapshot.json @@ -0,0 +1,7258 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "319572f5-d811-4091-aef8-6cca13f4ea75", + "prevId": "8f6d860f-ee22-4e2e-b913-25cec9faf4e9", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_workspaces_id": { + "name": "user_workspaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0009_snapshot.json b/drizzle/mysql/meta/0009_snapshot.json new file mode 100644 index 0000000..9cea0fd --- /dev/null +++ b/drizzle/mysql/meta/0009_snapshot.json @@ -0,0 +1,7356 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "90c31dd1-06ac-4ca5-891f-c971987963f4", + "prevId": "319572f5-d811-4091-aef8-6cca13f4ea75", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_workspaces_id": { + "name": "user_workspaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0010_snapshot.json b/drizzle/mysql/meta/0010_snapshot.json new file mode 100644 index 0000000..a608d98 --- /dev/null +++ b/drizzle/mysql/meta/0010_snapshot.json @@ -0,0 +1,7409 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "62dd3143-9a1c-472b-907b-ccefa7973916", + "prevId": "90c31dd1-06ac-4ca5-891f-c971987963f4", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_workspaces_id": { + "name": "user_workspaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0011_snapshot.json b/drizzle/mysql/meta/0011_snapshot.json new file mode 100644 index 0000000..5c0040f --- /dev/null +++ b/drizzle/mysql/meta/0011_snapshot.json @@ -0,0 +1,7639 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "b39562dc-20f4-4c06-99d8-ab57622376c7", + "prevId": "62dd3143-9a1c-472b-907b-ccefa7973916", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_workspaces_id": { + "name": "user_workspaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0012_snapshot.json b/drizzle/mysql/meta/0012_snapshot.json new file mode 100644 index 0000000..07beae3 --- /dev/null +++ b/drizzle/mysql/meta/0012_snapshot.json @@ -0,0 +1,8758 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a615700d-8b91-4e11-abcd-a35ab807904d", + "prevId": "b39562dc-20f4-4c06-99d8-ab57622376c7", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_conversations_id": { + "name": "ai_conversations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('')" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_messages_id": { + "name": "ai_messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_proposals_id": { + "name": "ai_proposals_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_channels_id": { + "name": "automation_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_run_steps_id": { + "name": "automation_run_steps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_runs_id": { + "name": "automation_runs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_schedules_id": { + "name": "automation_schedules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_trigger_state_id": { + "name": "automation_trigger_state_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('skip')" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automations_id": { + "name": "automations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_workspaces_id": { + "name": "user_workspaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0013_snapshot.json b/drizzle/mysql/meta/0013_snapshot.json new file mode 100644 index 0000000..51223dd --- /dev/null +++ b/drizzle/mysql/meta/0013_snapshot.json @@ -0,0 +1,8772 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "1ec357eb-b3c9-4025-937a-981e6479f867", + "prevId": "a615700d-8b91-4e11-abcd-a35ab807904d", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_conversations_id": { + "name": "ai_conversations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('')" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_messages_id": { + "name": "ai_messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_proposals_id": { + "name": "ai_proposals_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_channels_id": { + "name": "automation_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_run_steps_id": { + "name": "automation_run_steps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_runs_id": { + "name": "automation_runs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_schedules_id": { + "name": "automation_schedules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_trigger_state_id": { + "name": "automation_trigger_state_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('skip')" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automations_id": { + "name": "automations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_workspaces_id": { + "name": "user_workspaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0014_snapshot.json b/drizzle/mysql/meta/0014_snapshot.json new file mode 100644 index 0000000..4f633b3 --- /dev/null +++ b/drizzle/mysql/meta/0014_snapshot.json @@ -0,0 +1,8779 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "18a71d34-625b-452c-9035-b1b78df110f3", + "prevId": "1ec357eb-b3c9-4025-937a-981e6479f867", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_conversations_id": { + "name": "ai_conversations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('')" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_messages_id": { + "name": "ai_messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_proposals_id": { + "name": "ai_proposals_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('warning')" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_firings_id": { + "name": "alert_firings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rule_channels_id": { + "name": "alert_rule_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "alert_rules_id": { + "name": "alert_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_channels_id": { + "name": "automation_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_run_steps_id": { + "name": "automation_run_steps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_runs_id": { + "name": "automation_runs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_schedules_id": { + "name": "automation_schedules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_trigger_state_id": { + "name": "automation_trigger_state_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('skip')" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automations_id": { + "name": "automations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "c2s_tunnel_presets_id": { + "name": "c2s_tunnel_presets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "command_history_id": { + "name": "command_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_pinned_id": { + "name": "file_manager_pinned_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_recent_id": { + "name": "file_manager_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_manager_shortcuts_id": { + "name": "file_manager_shortcuts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_inventory_id": { + "name": "fleet_inventory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleet_members_id": { + "name": "fleet_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fleets_id": { + "name": "fleets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "network_topology_id": { + "name": "network_topology_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_node_history_id": { + "name": "proxmox_node_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proxmox_stats_preferences_id": { + "name": "proxmox_stats_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('view')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_access_id": { + "name": "snippet_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippet_folders_id": { + "name": "snippet_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "snippets_id": { + "name": "snippets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "transfer_recent_id": { + "name": "transfer_recent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_workspaces_id": { + "name": "user_workspaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json new file mode 100644 index 0000000..0d80a61 --- /dev/null +++ b/drizzle/mysql/meta/_journal.json @@ -0,0 +1,111 @@ +{ + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1785738871436, + "tag": "0000_clean_pretty_boy", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1786132418625, + "tag": "0001_puzzling_aqueduct", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1786147319261, + "tag": "0002_bumpy_korg", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1786258964346, + "tag": "0003_dry_human_robot", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1786423595505, + "tag": "0004_fancy_spencer_smythe", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1786428085301, + "tag": "0005_tearful_mindworm", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1786482021452, + "tag": "0006_last_fantastic_four", + "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1786487754919, + "tag": "0007_aspiring_turbo", + "breakpoints": true + }, + { + "idx": 8, + "version": "5", + "when": 1786498680274, + "tag": "0008_lame_nighthawk", + "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1786509736628, + "tag": "0009_tan_skaar", + "breakpoints": true + }, + { + "idx": 10, + "version": "5", + "when": 1786515117582, + "tag": "0010_small_infant_terrible", + "breakpoints": true + }, + { + "idx": 11, + "version": "5", + "when": 1786519424221, + "tag": "0011_easy_the_order", + "breakpoints": true + }, + { + "idx": 12, + "version": "5", + "when": 1786598522577, + "tag": "0012_outgoing_ultron", + "breakpoints": true + }, + { + "idx": 13, + "version": "5", + "when": 1786737725732, + "tag": "0013_third_wraith", + "breakpoints": true + }, + { + "idx": 14, + "version": "5", + "when": 1786757023790, + "tag": "0014_bitter_nextwave", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/postgres/0000_jazzy_infant_terrible.sql b/drizzle/postgres/0000_jazzy_infant_terrible.sql new file mode 100644 index 0000000..dd4c931 --- /dev/null +++ b/drizzle/postgres/0000_jazzy_infant_terrible.sql @@ -0,0 +1,837 @@ +CREATE TABLE "alert_firings" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "rule_id" integer NOT NULL, + "host_id" integer NOT NULL, + "host_name" text NOT NULL, + "fired_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "resolved_at" text, + "value" double precision, + "message" text NOT NULL, + "severity" text DEFAULT 'warning' NOT NULL, + "acknowledged" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "alert_rule_channels" ( + "id" serial PRIMARY KEY NOT NULL, + "rule_id" integer NOT NULL, + "channel_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE "alert_rules" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer, + "name" varchar(255) NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "trigger_type" text NOT NULL, + "threshold_value" double precision, + "threshold_duration_seconds" integer, + "cooldown_minutes" integer DEFAULT 15 NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "api_keys" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "token_hash" text NOT NULL, + "token_prefix" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "expires_at" text, + "last_used_at" text, + "is_active" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE "audit_logs" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255), + "username" text NOT NULL, + "action" text NOT NULL, + "resource_type" text NOT NULL, + "resource_id" text, + "resource_name" text, + "details" text, + "ip_address" text, + "user_agent" text, + "success" boolean NOT NULL, + "error_message" text, + "timestamp" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "c2s_tunnel_presets" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "config" text NOT NULL, + "platform" text, + "computer_name" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "command_history" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "command" text NOT NULL, + "executed_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "dashboard_service_links" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "label" text NOT NULL, + "url" text NOT NULL, + "order" integer DEFAULT 0 NOT NULL, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "dashboard_service_links_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "dismissed_alerts" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "alert_id" text NOT NULL, + "dismissed_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "file_manager_pinned" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "name" varchar(255) NOT NULL, + "path" text NOT NULL, + "pinned_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "file_manager_recent" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "name" varchar(255) NOT NULL, + "path" text NOT NULL, + "last_opened" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "file_manager_shortcuts" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "name" varchar(255) NOT NULL, + "path" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "homepage_items" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "type_id" text NOT NULL, + "title" text, + "config" text DEFAULT '{}' NOT NULL, + "folder_id" integer, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "homepage_items_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "homepage_layouts" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "layout" text DEFAULT '{}' NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "homepage_layouts_user_id_unique" UNIQUE("user_id") +); +--> statement-breakpoint +CREATE TABLE "host_access" ( + "id" serial PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "user_id" varchar(255), + "role_id" integer, + "granted_by" varchar(255) NOT NULL, + "permission_level" text DEFAULT 'connect' NOT NULL, + "expires_at" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "last_accessed_at" text, + "access_count" integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE "host_health_checks" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "checks" text NOT NULL, + "interval_seconds" integer DEFAULT 300 NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "host_health_history" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "check_id" text NOT NULL, + "ts" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "ok" boolean NOT NULL, + "latency_ms" integer, + "detail" text +); +--> statement-breakpoint +CREATE TABLE "host_metrics_history" ( + "id" serial PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "ts" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "cpu_percent" double precision, + "mem_percent" double precision, + "disk_percent" double precision, + "net_rx_bytes" integer, + "net_tx_bytes" integer +); +--> statement-breakpoint +CREATE TABLE "host_metrics_preferences" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "layout" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ssh_data" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "connection_type" text DEFAULT 'ssh' NOT NULL, + "name" varchar(255), + "ip" text NOT NULL, + "port" integer NOT NULL, + "username" text NOT NULL, + "folder" text, + "tags" text, + "pin" boolean DEFAULT false NOT NULL, + "auth_type" text NOT NULL, + "use_warpgate" boolean DEFAULT false NOT NULL, + "share_ssh_auth" boolean DEFAULT false NOT NULL, + "force_keyboard_interactive" text, + "password" text, + "key" text, + "key_password" text, + "key_type" text, + "sudo_password" text, + "autostart_password" text, + "autostart_key" text, + "autostart_key_password" text, + "credential_id" integer, + "override_credential_username" boolean, + "vault_profile_id" integer, + "enable_terminal" boolean DEFAULT true NOT NULL, + "enable_session_logging" boolean DEFAULT true NOT NULL, + "allow_session_sharing" boolean DEFAULT true NOT NULL, + "enable_command_history" boolean DEFAULT true NOT NULL, + "enable_tunnel" boolean DEFAULT true NOT NULL, + "tunnel_connections" text, + "jump_hosts" text, + "enable_file_manager" boolean DEFAULT true NOT NULL, + "scp_legacy" boolean DEFAULT false NOT NULL, + "enable_docker" boolean DEFAULT false NOT NULL, + "enable_tmux_monitor" boolean DEFAULT false NOT NULL, + "show_terminal_in_sidebar" boolean DEFAULT true NOT NULL, + "show_file_manager_in_sidebar" boolean DEFAULT false NOT NULL, + "show_tunnel_in_sidebar" boolean DEFAULT false NOT NULL, + "show_docker_in_sidebar" boolean DEFAULT false NOT NULL, + "show_server_stats_in_sidebar" boolean DEFAULT false NOT NULL, + "default_path" text, + "stats_config" text, + "docker_config" text, + "enable_proxmox" boolean DEFAULT false NOT NULL, + "proxmox_config" text, + "terminal_config" text, + "quick_actions" text, + "notes" text, + "enable_ssh" boolean DEFAULT true NOT NULL, + "enable_rdp" boolean DEFAULT false NOT NULL, + "enable_vnc" boolean DEFAULT false NOT NULL, + "enable_telnet" boolean DEFAULT false NOT NULL, + "ssh_port" integer DEFAULT 22, + "rdp_port" integer DEFAULT 3389, + "vnc_port" integer DEFAULT 5900, + "telnet_port" integer DEFAULT 23, + "rdp_credential_id" integer, + "rdp_user" text, + "rdp_password" text, + "rdp_domain" text, + "rdp_security" text, + "rdp_ignore_cert" boolean DEFAULT false, + "vnc_credential_id" integer, + "vnc_password" text, + "vnc_user" text, + "telnet_user" text, + "telnet_password" text, + "telnet_credential_id" integer, + "rdp_auth_type" text, + "vnc_auth_type" text, + "telnet_auth_type" text, + "domain" text, + "security" text, + "ignore_cert" boolean DEFAULT false, + "guacamole_config" text, + "use_socks5" boolean, + "socks5_host" text, + "socks5_port" integer, + "socks5_username" text, + "socks5_password" text, + "socks5_proxy_chain" text, + "connection_origin" text, + "mac_address" text, + "wol_broadcast_address" text, + "port_knock_sequence" text, + "host_key_fingerprint" text, + "host_key_type" text, + "host_key_algorithm" text DEFAULT 'sha256', + "host_key_first_seen" text, + "host_key_last_verified" text, + "host_key_changed_count" integer DEFAULT 0, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "ssh_data_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "network_topology" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "topology" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notification_channels" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "type" text NOT NULL, + "config" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "opkssh_tokens" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "ssh_cert" text NOT NULL, + "private_key" text NOT NULL, + "email" text, + "sub" text, + "issuer" text, + "audience" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "expires_at" text NOT NULL, + "last_used" text +); +--> statement-breakpoint +CREATE TABLE "recent_activity" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "type" text NOT NULL, + "host_id" integer NOT NULL, + "host_name" text, + "timestamp" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "roles" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "display_name" text NOT NULL, + "description" text, + "is_system" boolean DEFAULT false NOT NULL, + "permissions" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "roles_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "session_recordings" ( + "id" serial PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "user_id" varchar(255), + "username" text, + "access_id" integer, + "started_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "ended_at" text, + "duration" integer, + "commands" text, + "dangerous_actions" text, + "recording_path" text, + "protocol" varchar(255) DEFAULT 'ssh' NOT NULL, + "format" text DEFAULT 'text' NOT NULL, + "terminated_by_owner" boolean DEFAULT false, + "termination_reason" text +); +--> statement-breakpoint +CREATE TABLE "session_share_participants" ( + "id" serial PRIMARY KEY NOT NULL, + "share_id" varchar(255) NOT NULL, + "user_id" varchar(255), + "guest_label" text, + "joined_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "left_at" text +); +--> statement-breakpoint +CREATE TABLE "session_shares" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "owner_user_id" varchar(255) NOT NULL, + "protocol" varchar(255) NOT NULL, + "session_id" text NOT NULL, + "tab_instance_id" text, + "share_type" text NOT NULL, + "target_user_id" varchar(255), + "link_token" varchar(255), + "permission_level" text DEFAULT 'read-only' NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "expires_at" text NOT NULL, + "revoked_at" text, + "last_joined_at" text, + "join_count" integer DEFAULT 0 NOT NULL, + CONSTRAINT "session_shares_link_token_unique" UNIQUE("link_token") +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "jwt_token" text NOT NULL, + "device_type" text NOT NULL, + "device_info" text NOT NULL, + "oidc_sub" text, + "oidc_sid" text, + "sso_provider_id" integer, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "expires_at" text NOT NULL, + "last_active_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "settings" ( + "key" varchar(255) PRIMARY KEY NOT NULL, + "value" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "shared_host_auth_overrides" ( + "id" serial PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "protocol" varchar(255) DEFAULT 'ssh' NOT NULL, + "credential_id" integer NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "shared_host_secrets" ( + "id" serial PRIMARY KEY NOT NULL, + "host_access_id" integer NOT NULL, + "target_user_id" varchar(255) NOT NULL, + "protocol" varchar(255) DEFAULT 'ssh' NOT NULL, + "source_type" text DEFAULT 'credential' NOT NULL, + "original_credential_id" integer, + "encrypted_username" text, + "encrypted_auth_type" text, + "encrypted_password" text, + "encrypted_key" text, + "encrypted_key_password" text, + "encrypted_key_type" text, + "encrypted_domain" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "snippet_access" ( + "id" serial PRIMARY KEY NOT NULL, + "snippet_id" integer NOT NULL, + "user_id" varchar(255), + "role_id" integer, + "granted_by" varchar(255) NOT NULL, + "permission_level" text DEFAULT 'view' NOT NULL, + "expires_at" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "snippet_folders" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "color" text, + "icon" text, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "snippet_folders_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "snippets" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "content" text NOT NULL, + "description" text, + "folder" text, + "order" integer DEFAULT 0 NOT NULL, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "host_filter" text, + CONSTRAINT "snippets_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "ssh_credential_usage" ( + "id" serial PRIMARY KEY NOT NULL, + "credential_id" integer NOT NULL, + "host_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "used_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ssh_credentials" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "folder" text, + "tags" text, + "auth_type" text NOT NULL, + "username" text, + "password" text, + "key" text, + "private_key" text, + "public_key" text, + "key_password" text, + "key_type" text, + "detected_key_type" text, + "cert_public_key" text, + "usage_count" integer DEFAULT 0 NOT NULL, + "last_used" text, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "ssh_credentials_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "ssh_folders" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "color" text, + "icon" text, + "credential_id" integer, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "ssh_folders_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "sso_providers" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "type" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "display_order" integer DEFAULT 0 NOT NULL, + "config" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sync_tombstones" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "entity_type" text NOT NULL, + "sync_id" varchar(255) NOT NULL, + "deleted_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "termix_identities" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "handle" varchar(255) NOT NULL, + "description" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "termix_identities_user_id_unique" UNIQUE("user_id"), + CONSTRAINT "termix_identities_handle_unique" UNIQUE("handle") +); +--> statement-breakpoint +CREATE TABLE "termix_identity_ca" ( + "id" serial PRIMARY KEY NOT NULL, + "identity_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "public_key" text NOT NULL, + "private_key" text NOT NULL, + "validity_days" integer DEFAULT 90 NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "termix_identity_ca_identity_id_unique" UNIQUE("identity_id") +); +--> statement-breakpoint +CREATE TABLE "termix_identity_keys" ( + "id" serial PRIMARY KEY NOT NULL, + "identity_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "public_key" text NOT NULL, + "key_type" text NOT NULL, + "algorithm" text NOT NULL, + "label" text, + "comment" text, + "source" text DEFAULT 'manual' NOT NULL, + "credential_id" integer, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "tmux_session_tags" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "session_name" text NOT NULL, + "tag" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "transfer_recent" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "source_host_id" integer NOT NULL, + "dest_host_id" integer NOT NULL, + "dest_path" text NOT NULL, + "dest_path_label" text NOT NULL, + "last_used" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "trusted_devices" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "device_fingerprint" text NOT NULL, + "device_type" text NOT NULL, + "device_info" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "expires_at" text NOT NULL, + "last_used_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_open_tabs" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "tab_type" text NOT NULL, + "host_id" integer, + "label" text NOT NULL, + "tab_order" integer DEFAULT 0 NOT NULL, + "backend_session_id" text, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_preferences" ( + "user_id" varchar(255) PRIMARY KEY NOT NULL, + "reopen_tabs_on_login" boolean DEFAULT false NOT NULL, + "theme" text, + "font_size" text, + "accent_color" text, + "language" text, + "storage_mode" text, + "command_autocomplete" boolean, + "command_palette_enabled" boolean, + "show_host_tags" boolean, + "host_tray_on_click" boolean, + "pin_app_rail" boolean, + "expand_app_rail_on_hover" boolean, + "folders_collapsed" boolean, + "confirm_snippet_execution" boolean, + "disable_update_check" boolean, + "confirm_tab_close" boolean, + "hidden_rail_tabs" text, + "compact_host_view" boolean, + "status_color_scheme" text, + "custom_themes" text, + "custom_keybindings" text, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_roles" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "role_id" integer NOT NULL, + "granted_by" varchar(255), + "granted_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "username" text NOT NULL, + "password_hash" text NOT NULL, + "is_admin" boolean DEFAULT false NOT NULL, + "is_oidc" boolean DEFAULT false NOT NULL, + "oidc_identifier" text, + "sso_provider_id" integer, + "client_id" text, + "client_secret" text, + "issuer_url" text, + "authorization_url" text, + "token_url" text, + "identifier_path" text, + "name_path" text, + "scopes" text DEFAULT 'openid email profile', + "totp_secret" text, + "totp_enabled" boolean DEFAULT false NOT NULL, + "totp_backup_codes" text, + "registered_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "donation_modal_dismissed" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "vault_profiles" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "folder" text, + "tags" text, + "vault_addr" text NOT NULL, + "vault_namespace" text, + "oidc_mount" text, + "oidc_role" text, + "ssh_mount" text, + "ssh_role" text NOT NULL, + "valid_principals" text, + "key_type" text, + "shared" boolean DEFAULT false NOT NULL, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "vault_profiles_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +CREATE TABLE "vault_tokens" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "profile_id" integer NOT NULL, + "ssh_cert" text NOT NULL, + "private_key" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "expires_at" text NOT NULL, + "last_used" text +); +--> statement-breakpoint +CREATE TABLE "webauthn_credentials" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "credential_id" text NOT NULL, + "public_key" text NOT NULL, + "counter" integer DEFAULT 0 NOT NULL, + "device_type" text, + "backed_up" boolean DEFAULT false NOT NULL, + "transports" text, + "user_verification" text DEFAULT 'preferred' NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "last_used_at" text +); +--> statement-breakpoint +ALTER TABLE "alert_firings" ADD CONSTRAINT "alert_firings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_firings" ADD CONSTRAINT "alert_firings_rule_id_alert_rules_id_fk" FOREIGN KEY ("rule_id") REFERENCES "public"."alert_rules"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_rule_channels" ADD CONSTRAINT "alert_rule_channels_rule_id_alert_rules_id_fk" FOREIGN KEY ("rule_id") REFERENCES "public"."alert_rules"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_rule_channels" ADD CONSTRAINT "alert_rule_channels_channel_id_notification_channels_id_fk" FOREIGN KEY ("channel_id") REFERENCES "public"."notification_channels"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_rules" ADD CONSTRAINT "alert_rules_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_rules" ADD CONSTRAINT "alert_rules_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "c2s_tunnel_presets" ADD CONSTRAINT "c2s_tunnel_presets_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "command_history" ADD CONSTRAINT "command_history_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "command_history" ADD CONSTRAINT "command_history_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dashboard_service_links" ADD CONSTRAINT "dashboard_service_links_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dismissed_alerts" ADD CONSTRAINT "dismissed_alerts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "file_manager_pinned" ADD CONSTRAINT "file_manager_pinned_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "file_manager_pinned" ADD CONSTRAINT "file_manager_pinned_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "file_manager_recent" ADD CONSTRAINT "file_manager_recent_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "file_manager_recent" ADD CONSTRAINT "file_manager_recent_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "file_manager_shortcuts" ADD CONSTRAINT "file_manager_shortcuts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "file_manager_shortcuts" ADD CONSTRAINT "file_manager_shortcuts_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "homepage_items" ADD CONSTRAINT "homepage_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "homepage_layouts" ADD CONSTRAINT "homepage_layouts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_access" ADD CONSTRAINT "host_access_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_access" ADD CONSTRAINT "host_access_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_access" ADD CONSTRAINT "host_access_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_access" ADD CONSTRAINT "host_access_granted_by_users_id_fk" FOREIGN KEY ("granted_by") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_health_checks" ADD CONSTRAINT "host_health_checks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_health_checks" ADD CONSTRAINT "host_health_checks_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_health_history" ADD CONSTRAINT "host_health_history_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_health_history" ADD CONSTRAINT "host_health_history_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_metrics_history" ADD CONSTRAINT "host_metrics_history_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_metrics_preferences" ADD CONSTRAINT "host_metrics_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_metrics_preferences" ADD CONSTRAINT "host_metrics_preferences_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD CONSTRAINT "ssh_data_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD CONSTRAINT "ssh_data_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD CONSTRAINT "ssh_data_vault_profile_id_vault_profiles_id_fk" FOREIGN KEY ("vault_profile_id") REFERENCES "public"."vault_profiles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD CONSTRAINT "ssh_data_rdp_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("rdp_credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD CONSTRAINT "ssh_data_vnc_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("vnc_credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD CONSTRAINT "ssh_data_telnet_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("telnet_credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "network_topology" ADD CONSTRAINT "network_topology_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notification_channels" ADD CONSTRAINT "notification_channels_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "opkssh_tokens" ADD CONSTRAINT "opkssh_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "opkssh_tokens" ADD CONSTRAINT "opkssh_tokens_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "recent_activity" ADD CONSTRAINT "recent_activity_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "recent_activity" ADD CONSTRAINT "recent_activity_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_recordings" ADD CONSTRAINT "session_recordings_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_recordings" ADD CONSTRAINT "session_recordings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_recordings" ADD CONSTRAINT "session_recordings_access_id_host_access_id_fk" FOREIGN KEY ("access_id") REFERENCES "public"."host_access"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_share_participants" ADD CONSTRAINT "session_share_participants_share_id_session_shares_id_fk" FOREIGN KEY ("share_id") REFERENCES "public"."session_shares"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_share_participants" ADD CONSTRAINT "session_share_participants_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_shares" ADD CONSTRAINT "session_shares_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_shares" ADD CONSTRAINT "session_shares_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_shares" ADD CONSTRAINT "session_shares_target_user_id_users_id_fk" FOREIGN KEY ("target_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shared_host_auth_overrides" ADD CONSTRAINT "shared_host_auth_overrides_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shared_host_auth_overrides" ADD CONSTRAINT "shared_host_auth_overrides_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shared_host_auth_overrides" ADD CONSTRAINT "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shared_host_secrets" ADD CONSTRAINT "shared_host_secrets_host_access_id_host_access_id_fk" FOREIGN KEY ("host_access_id") REFERENCES "public"."host_access"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shared_host_secrets" ADD CONSTRAINT "shared_host_secrets_target_user_id_users_id_fk" FOREIGN KEY ("target_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shared_host_secrets" ADD CONSTRAINT "shared_host_secrets_original_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("original_credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "snippet_access" ADD CONSTRAINT "snippet_access_snippet_id_snippets_id_fk" FOREIGN KEY ("snippet_id") REFERENCES "public"."snippets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "snippet_access" ADD CONSTRAINT "snippet_access_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "snippet_access" ADD CONSTRAINT "snippet_access_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "snippet_access" ADD CONSTRAINT "snippet_access_granted_by_users_id_fk" FOREIGN KEY ("granted_by") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "snippet_folders" ADD CONSTRAINT "snippet_folders_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "snippets" ADD CONSTRAINT "snippets_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_credential_usage" ADD CONSTRAINT "ssh_credential_usage_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_credential_usage" ADD CONSTRAINT "ssh_credential_usage_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_credential_usage" ADD CONSTRAINT "ssh_credential_usage_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_credentials" ADD CONSTRAINT "ssh_credentials_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_folders" ADD CONSTRAINT "ssh_folders_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh_folders" ADD CONSTRAINT "ssh_folders_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sync_tombstones" ADD CONSTRAINT "sync_tombstones_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "termix_identities" ADD CONSTRAINT "termix_identities_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "termix_identity_ca" ADD CONSTRAINT "termix_identity_ca_identity_id_termix_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "public"."termix_identities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "termix_identity_ca" ADD CONSTRAINT "termix_identity_ca_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "termix_identity_keys" ADD CONSTRAINT "termix_identity_keys_identity_id_termix_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "public"."termix_identities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "termix_identity_keys" ADD CONSTRAINT "termix_identity_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "termix_identity_keys" ADD CONSTRAINT "termix_identity_keys_credential_id_ssh_credentials_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."ssh_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tmux_session_tags" ADD CONSTRAINT "tmux_session_tags_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tmux_session_tags" ADD CONSTRAINT "tmux_session_tags_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "transfer_recent" ADD CONSTRAINT "transfer_recent_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "transfer_recent" ADD CONSTRAINT "transfer_recent_source_host_id_ssh_data_id_fk" FOREIGN KEY ("source_host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "transfer_recent" ADD CONSTRAINT "transfer_recent_dest_host_id_ssh_data_id_fk" FOREIGN KEY ("dest_host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "trusted_devices" ADD CONSTRAINT "trusted_devices_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_open_tabs" ADD CONSTRAINT "user_open_tabs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_open_tabs" ADD CONSTRAINT "user_open_tabs_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_granted_by_users_id_fk" FOREIGN KEY ("granted_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vault_profiles" ADD CONSTRAINT "vault_profiles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vault_tokens" ADD CONSTRAINT "vault_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vault_tokens" ADD CONSTRAINT "vault_tokens_profile_id_vault_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."vault_profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "webauthn_credentials" ADD CONSTRAINT "webauthn_credentials_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "idx_host_health_checks_user_host" ON "host_health_checks" USING btree ("user_id","host_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_host_metrics_prefs_user_host" ON "host_metrics_preferences" USING btree ("user_id","host_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_opkssh_tokens_user_host" ON "opkssh_tokens" USING btree ("user_id","host_id");--> statement-breakpoint +CREATE UNIQUE INDEX "shared_host_auth_overrides_host_user_protocol_unique" ON "shared_host_auth_overrides" USING btree ("host_id","user_id","protocol");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_shared_host_secrets_scope" ON "shared_host_secrets" USING btree ("host_access_id","target_user_id","protocol");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_user_roles_user_role" ON "user_roles" USING btree ("user_id","role_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_vault_tokens_user_profile" ON "vault_tokens" USING btree ("user_id","profile_id"); \ No newline at end of file diff --git a/drizzle/postgres/0001_worried_silvermane.sql b/drizzle/postgres/0001_worried_silvermane.sql new file mode 100644 index 0000000..5164820 --- /dev/null +++ b/drizzle/postgres/0001_worried_silvermane.sql @@ -0,0 +1,9 @@ +CREATE TABLE "host_sidebar_preferences" ( + "user_id" varchar(255) PRIMARY KEY NOT NULL, + "data" text NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "ssh_data" ADD COLUMN "sort_order" integer;--> statement-breakpoint +ALTER TABLE "ssh_folders" ADD COLUMN "sort_order" integer;--> statement-breakpoint +ALTER TABLE "host_sidebar_preferences" ADD CONSTRAINT "host_sidebar_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/postgres/0002_clear_cerebro.sql b/drizzle/postgres/0002_clear_cerebro.sql new file mode 100644 index 0000000..0910be5 --- /dev/null +++ b/drizzle/postgres/0002_clear_cerebro.sql @@ -0,0 +1,9 @@ +CREATE TABLE "credential_sidebar_preferences" ( + "user_id" varchar(255) PRIMARY KEY NOT NULL, + "data" text NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "ssh_credentials" ADD COLUMN "pin" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "ssh_credentials" ADD COLUMN "sort_order" integer;--> statement-breakpoint +ALTER TABLE "credential_sidebar_preferences" ADD CONSTRAINT "credential_sidebar_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/postgres/0003_harsh_gravity.sql b/drizzle/postgres/0003_harsh_gravity.sql new file mode 100644 index 0000000..0d49e05 --- /dev/null +++ b/drizzle/postgres/0003_harsh_gravity.sql @@ -0,0 +1 @@ +ALTER TABLE "snippets" ADD COLUMN "is_note" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/postgres/0004_great_victor_mancha.sql b/drizzle/postgres/0004_great_victor_mancha.sql new file mode 100644 index 0000000..7b414a8 --- /dev/null +++ b/drizzle/postgres/0004_great_victor_mancha.sql @@ -0,0 +1,26 @@ +CREATE TABLE "proxmox_node_history" ( + "id" serial PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "ts" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "cpu_percent" double precision, + "mem_percent" double precision, + "disk_percent" double precision, + "net_rx_bytes" integer, + "net_tx_bytes" integer +); +--> statement-breakpoint +CREATE TABLE "proxmox_stats_preferences" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "layout" text NOT NULL, + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "ssh_data" ADD COLUMN "enable_proxmox_stats" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD COLUMN "proxmox_stats_config" text;--> statement-breakpoint +ALTER TABLE "proxmox_node_history" ADD CONSTRAINT "proxmox_node_history_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "proxmox_stats_preferences" ADD CONSTRAINT "proxmox_stats_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "proxmox_stats_preferences" ADD CONSTRAINT "proxmox_stats_preferences_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "idx_proxmox_stats_prefs_user_host" ON "proxmox_stats_preferences" USING btree ("user_id","host_id"); \ No newline at end of file diff --git a/drizzle/postgres/0005_loose_captain_marvel.sql b/drizzle/postgres/0005_loose_captain_marvel.sql new file mode 100644 index 0000000..ed87ffc --- /dev/null +++ b/drizzle/postgres/0005_loose_captain_marvel.sql @@ -0,0 +1 @@ +ALTER TABLE "ssh_data" ADD COLUMN "enable_terminal_toolbar" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/drizzle/postgres/0006_gigantic_thor_girl.sql b/drizzle/postgres/0006_gigantic_thor_girl.sql new file mode 100644 index 0000000..7496d3c --- /dev/null +++ b/drizzle/postgres/0006_gigantic_thor_girl.sql @@ -0,0 +1,42 @@ +CREATE TABLE "fleet_inventory" ( + "id" serial PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "os_pretty_name" text, + "kernel" text, + "architecture" text, + "hostname" text, + "uptime_seconds" integer, + "ip" text, + "package_manager" text, + "collected_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "fleet_members" ( + "id" serial PRIMARY KEY NOT NULL, + "fleet_id" integer NOT NULL, + "host_id" integer NOT NULL, + "added_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "fleets" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "color" text, + "icon" text, + "tag_rules" text, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "fleets_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +ALTER TABLE "fleet_inventory" ADD CONSTRAINT "fleet_inventory_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fleet_inventory" ADD CONSTRAINT "fleet_inventory_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fleet_members" ADD CONSTRAINT "fleet_members_fleet_id_fleets_id_fk" FOREIGN KEY ("fleet_id") REFERENCES "public"."fleets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fleet_members" ADD CONSTRAINT "fleet_members_host_id_ssh_data_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."ssh_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fleets" ADD CONSTRAINT "fleets_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "idx_fleet_inventory_host" ON "fleet_inventory" USING btree ("host_id","user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_fleet_members_fleet_host" ON "fleet_members" USING btree ("fleet_id","host_id"); \ No newline at end of file diff --git a/drizzle/postgres/0007_orange_mandrill.sql b/drizzle/postgres/0007_orange_mandrill.sql new file mode 100644 index 0000000..352912f --- /dev/null +++ b/drizzle/postgres/0007_orange_mandrill.sql @@ -0,0 +1,2 @@ +ALTER TABLE "ssh_data" ADD COLUMN "parent_host_id" integer;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD CONSTRAINT "ssh_data_parent_host_id_ssh_data_id_fk" FOREIGN KEY ("parent_host_id") REFERENCES "public"."ssh_data"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/postgres/0008_bright_miss_america.sql b/drizzle/postgres/0008_bright_miss_america.sql new file mode 100644 index 0000000..9f100bf --- /dev/null +++ b/drizzle/postgres/0008_bright_miss_america.sql @@ -0,0 +1,17 @@ +CREATE TABLE "user_workspaces" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "color" text, + "icon" text, + "kind" text DEFAULT 'manual' NOT NULL, + "is_default" boolean DEFAULT false NOT NULL, + "payload" text DEFAULT '{}' NOT NULL, + "sync_id" varchar(255), + "created_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "last_used_at" text, + CONSTRAINT "user_workspaces_sync_id_unique" UNIQUE("sync_id") +); +--> statement-breakpoint +ALTER TABLE "user_workspaces" ADD CONSTRAINT "user_workspaces_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/postgres/0009_spicy_the_leader.sql b/drizzle/postgres/0009_spicy_the_leader.sql new file mode 100644 index 0000000..91a25f6 --- /dev/null +++ b/drizzle/postgres/0009_spicy_the_leader.sql @@ -0,0 +1,28 @@ +ALTER TABLE "api_keys" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "audit_logs" ALTER COLUMN "action" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "audit_logs" ALTER COLUMN "resource_type" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "audit_logs" ALTER COLUMN "timestamp" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "audit_logs" ALTER COLUMN "timestamp" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "host_access" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "opkssh_tokens" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "recent_activity" ALTER COLUMN "timestamp" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "recent_activity" ALTER COLUMN "timestamp" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "session_shares" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "sessions" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "snippet_access" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "trusted_devices" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "vault_tokens" ALTER COLUMN "expires_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "webauthn_credentials" ALTER COLUMN "credential_id" SET DATA TYPE varchar(255);--> statement-breakpoint +CREATE INDEX "idx_audit_logs_timestamp" ON "audit_logs" USING btree ("timestamp");--> statement-breakpoint +CREATE INDEX "idx_audit_logs_user_ts" ON "audit_logs" USING btree ("user_id","timestamp");--> statement-breakpoint +CREATE INDEX "idx_audit_logs_action_ts" ON "audit_logs" USING btree ("action","timestamp");--> statement-breakpoint +CREATE INDEX "idx_audit_logs_resource_ts" ON "audit_logs" USING btree ("resource_type","timestamp");--> statement-breakpoint +CREATE INDEX "idx_host_access_user_id" ON "host_access" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_host_access_role_id" ON "host_access" USING btree ("role_id");--> statement-breakpoint +CREATE INDEX "idx_host_access_host_id" ON "host_access" USING btree ("host_id");--> statement-breakpoint +CREATE INDEX "idx_host_access_expires_at" ON "host_access" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "idx_ssh_data_user_id" ON "ssh_data" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_ssh_data_parent_host" ON "ssh_data" USING btree ("parent_host_id");--> statement-breakpoint +CREATE INDEX "idx_ssh_data_credential" ON "ssh_data" USING btree ("credential_id");--> statement-breakpoint +CREATE INDEX "idx_sessions_user_id" ON "sessions" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_sessions_expires_at" ON "sessions" USING btree ("expires_at"); \ No newline at end of file diff --git a/drizzle/postgres/0010_nasty_mordo.sql b/drizzle/postgres/0010_nasty_mordo.sql new file mode 100644 index 0000000..27193da --- /dev/null +++ b/drizzle/postgres/0010_nasty_mordo.sql @@ -0,0 +1,7 @@ +CREATE TABLE "ui_preferences" ( + "user_id" varchar(255) PRIMARY KEY NOT NULL, + "data" text NOT NULL, + "updated_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "ui_preferences" ADD CONSTRAINT "ui_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/postgres/0011_unique_sleepwalker.sql b/drizzle/postgres/0011_unique_sleepwalker.sql new file mode 100644 index 0000000..bc63bd0 --- /dev/null +++ b/drizzle/postgres/0011_unique_sleepwalker.sql @@ -0,0 +1,34 @@ +ALTER TABLE "alert_firings" ALTER COLUMN "fired_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "alert_firings" ALTER COLUMN "fired_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "session_recordings" ALTER COLUMN "started_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "session_recordings" ALTER COLUMN "started_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "session_shares" ALTER COLUMN "session_id" SET DATA TYPE varchar(255);--> statement-breakpoint +CREATE INDEX "idx_alert_firings_rule" ON "alert_firings" USING btree ("rule_id","fired_at");--> statement-breakpoint +CREATE INDEX "idx_alert_firings_host" ON "alert_firings" USING btree ("host_id");--> statement-breakpoint +CREATE INDEX "idx_api_keys_user_id" ON "api_keys" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_command_history_user_host" ON "command_history" USING btree ("user_id","host_id");--> statement-breakpoint +CREATE INDEX "idx_dismissed_alerts_user_id" ON "dismissed_alerts" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_file_manager_pinned_user" ON "file_manager_pinned" USING btree ("user_id","host_id");--> statement-breakpoint +CREATE INDEX "idx_file_manager_recent_user" ON "file_manager_recent" USING btree ("user_id","host_id");--> statement-breakpoint +CREATE INDEX "idx_file_manager_shortcuts_user" ON "file_manager_shortcuts" USING btree ("user_id","host_id");--> statement-breakpoint +CREATE INDEX "idx_fleet_inventory_user" ON "fleet_inventory" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_fleet_members_host" ON "fleet_members" USING btree ("host_id");--> statement-breakpoint +CREATE INDEX "idx_homepage_items_user_id" ON "homepage_items" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_recent_activity_user_ts" ON "recent_activity" USING btree ("user_id","timestamp");--> statement-breakpoint +CREATE INDEX "idx_session_recordings_user_started" ON "session_recordings" USING btree ("user_id","started_at");--> statement-breakpoint +CREATE INDEX "idx_session_recordings_host" ON "session_recordings" USING btree ("host_id");--> statement-breakpoint +CREATE INDEX "idx_session_shares_session_id" ON "session_shares" USING btree ("session_id");--> statement-breakpoint +CREATE INDEX "idx_session_shares_host_id" ON "session_shares" USING btree ("host_id");--> statement-breakpoint +CREATE INDEX "idx_snippet_access_user_id" ON "snippet_access" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_snippet_access_snippet_id" ON "snippet_access" USING btree ("snippet_id");--> statement-breakpoint +CREATE INDEX "idx_snippet_access_role_id" ON "snippet_access" USING btree ("role_id");--> statement-breakpoint +CREATE INDEX "idx_snippets_user_id" ON "snippets" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_ssh_credential_usage_credential" ON "ssh_credential_usage" USING btree ("credential_id");--> statement-breakpoint +CREATE INDEX "idx_ssh_credential_usage_user" ON "ssh_credential_usage" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_ssh_credentials_user_id" ON "ssh_credentials" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_ssh_folders_user_id" ON "ssh_folders" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_transfer_recent_user" ON "transfer_recent" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_trusted_devices_user_id" ON "trusted_devices" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_user_open_tabs_user_id" ON "user_open_tabs" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_user_roles_role_id" ON "user_roles" USING btree ("role_id");--> statement-breakpoint +CREATE INDEX "idx_user_workspaces_user_id" ON "user_workspaces" USING btree ("user_id"); \ No newline at end of file diff --git a/drizzle/postgres/0012_dashing_mandroid.sql b/drizzle/postgres/0012_dashing_mandroid.sql new file mode 100644 index 0000000..f999260 --- /dev/null +++ b/drizzle/postgres/0012_dashing_mandroid.sql @@ -0,0 +1,278 @@ +CREATE TABLE "ai_conversations" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "title" text, + "provider_id" integer, + "model" text, + "created_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ai_messages" ( + "id" serial PRIMARY KEY NOT NULL, + "conversation_id" integer NOT NULL, + "role" text NOT NULL, + "content" text DEFAULT '' NOT NULL, + "tool_calls" text, + "created_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ai_proposals" ( + "id" serial PRIMARY KEY NOT NULL, + "conversation_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "kind" text NOT NULL, + "summary" text, + "payload" text DEFAULT '{}' NOT NULL, + "status" varchar(255) DEFAULT 'pending' NOT NULL, + "applied_at" text, + "result_summary" text, + "created_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ai_providers" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "provider_type" text NOT NULL, + "label" varchar(255) NOT NULL, + "base_url" text, + "api_key" text, + "api_key_prefix" text, + "default_model" text, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "automation_channels" ( + "id" serial PRIMARY KEY NOT NULL, + "automation_id" integer NOT NULL, + "channel_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE "automation_run_steps" ( + "id" serial PRIMARY KEY NOT NULL, + "run_id" integer NOT NULL, + "step_index" integer NOT NULL, + "step_id" text NOT NULL, + "step_type" text NOT NULL, + "status" varchar(255) NOT NULL, + "started_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "finished_at" text, + "output" text, + "error" text, + "truncated" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "automation_runs" ( + "id" serial PRIMARY KEY NOT NULL, + "automation_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "trigger_type" text NOT NULL, + "trigger_context" text, + "status" varchar(255) NOT NULL, + "started_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "finished_at" text, + "duration_ms" integer, + "error" text, + "dry_run" boolean DEFAULT false NOT NULL, + "parent_run_id" integer +); +--> statement-breakpoint +CREATE TABLE "automation_schedules" ( + "id" serial PRIMARY KEY NOT NULL, + "automation_id" integer NOT NULL, + "cron" text, + "interval_seconds" integer, + "timezone" text, + "next_due_at" varchar(255), + "last_tick_at" text +); +--> statement-breakpoint +CREATE TABLE "automation_trigger_state" ( + "id" serial PRIMARY KEY NOT NULL, + "automation_id" integer NOT NULL, + "state_key" varchar(255) NOT NULL, + "breach_started_at" text, + "last_fired_at" text, + "last_value" double precision, + "last_observed_state" text, + "updated_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "automations" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "enabled" boolean DEFAULT true NOT NULL, + "definition" text NOT NULL, + "definition_version" integer DEFAULT 1 NOT NULL, + "concurrency_policy" text DEFAULT 'skip' NOT NULL, + "max_run_seconds" integer DEFAULT 300 NOT NULL, + "dry_run" boolean DEFAULT false NOT NULL, + "last_run_at" text, + "last_run_status" text, + "created_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "alert_rules" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "alert_rules" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "alert_rules" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "alert_rules" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "api_keys" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "api_keys" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "c2s_tunnel_presets" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "c2s_tunnel_presets" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "c2s_tunnel_presets" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "c2s_tunnel_presets" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "credential_sidebar_preferences" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "credential_sidebar_preferences" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "dashboard_service_links" ALTER COLUMN "label" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "dashboard_service_links" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "dashboard_service_links" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "dashboard_service_links" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "dashboard_service_links" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "file_manager_shortcuts" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "file_manager_shortcuts" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "fleets" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "fleets" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "fleets" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "fleets" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "homepage_items" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "homepage_items" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "homepage_items" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "homepage_items" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "homepage_layouts" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "homepage_layouts" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "host_access" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "host_access" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "host_health_checks" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "host_health_checks" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "host_health_checks" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "host_health_checks" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "host_metrics_preferences" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "host_metrics_preferences" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "host_metrics_preferences" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "host_metrics_preferences" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "host_sidebar_preferences" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "host_sidebar_preferences" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "ssh_data" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "ssh_data" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "ssh_data" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "ssh_data" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "network_topology" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "network_topology" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "network_topology" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "network_topology" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "notification_channels" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "notification_channels" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "opkssh_tokens" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "opkssh_tokens" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "proxmox_stats_preferences" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "proxmox_stats_preferences" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "proxmox_stats_preferences" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "proxmox_stats_preferences" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "roles" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "roles" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "roles" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "roles" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "session_shares" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "session_shares" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "sessions" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "sessions" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "shared_host_auth_overrides" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "shared_host_auth_overrides" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "shared_host_auth_overrides" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "shared_host_auth_overrides" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "shared_host_secrets" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "shared_host_secrets" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "shared_host_secrets" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "shared_host_secrets" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "snippet_access" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "snippet_access" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "snippet_folders" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "snippet_folders" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "snippet_folders" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "snippet_folders" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "snippets" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "snippets" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "snippets" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "snippets" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "ssh_credentials" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "ssh_credentials" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "ssh_credentials" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "ssh_credentials" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "ssh_folders" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "ssh_folders" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "ssh_folders" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "ssh_folders" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "sso_providers" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "sso_providers" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "sso_providers" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "sso_providers" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "termix_identities" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "termix_identities" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "termix_identities" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "termix_identities" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "termix_identity_ca" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "termix_identity_ca" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "termix_identity_ca" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "termix_identity_ca" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "termix_identity_keys" ALTER COLUMN "label" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "termix_identity_keys" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "termix_identity_keys" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "tmux_session_tags" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "tmux_session_tags" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "trusted_devices" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "trusted_devices" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "ui_preferences" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "ui_preferences" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "user_open_tabs" ALTER COLUMN "label" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "user_open_tabs" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "user_open_tabs" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "user_open_tabs" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "user_open_tabs" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "user_preferences" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "user_preferences" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "user_workspaces" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "user_workspaces" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "user_workspaces" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "user_workspaces" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "vault_profiles" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "vault_profiles" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "vault_profiles" ALTER COLUMN "updated_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "vault_profiles" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "vault_tokens" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "vault_tokens" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "webauthn_credentials" ALTER COLUMN "created_at" SET DATA TYPE varchar(255);--> statement-breakpoint +ALTER TABLE "webauthn_credentials" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD COLUMN "ai_assistant_enabled" boolean;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD COLUMN "ai_read_only_commands" boolean;--> statement-breakpoint +ALTER TABLE "ai_conversations" ADD CONSTRAINT "ai_conversations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_messages" ADD CONSTRAINT "ai_messages_conversation_id_ai_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."ai_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_proposals" ADD CONSTRAINT "ai_proposals_conversation_id_ai_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."ai_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_proposals" ADD CONSTRAINT "ai_proposals_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_providers" ADD CONSTRAINT "ai_providers_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automation_channels" ADD CONSTRAINT "automation_channels_automation_id_automations_id_fk" FOREIGN KEY ("automation_id") REFERENCES "public"."automations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automation_channels" ADD CONSTRAINT "automation_channels_channel_id_notification_channels_id_fk" FOREIGN KEY ("channel_id") REFERENCES "public"."notification_channels"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automation_run_steps" ADD CONSTRAINT "automation_run_steps_run_id_automation_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."automation_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automation_runs" ADD CONSTRAINT "automation_runs_automation_id_automations_id_fk" FOREIGN KEY ("automation_id") REFERENCES "public"."automations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automation_runs" ADD CONSTRAINT "automation_runs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automation_schedules" ADD CONSTRAINT "automation_schedules_automation_id_automations_id_fk" FOREIGN KEY ("automation_id") REFERENCES "public"."automations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automation_trigger_state" ADD CONSTRAINT "automation_trigger_state_automation_id_automations_id_fk" FOREIGN KEY ("automation_id") REFERENCES "public"."automations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "automations" ADD CONSTRAINT "automations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_ai_conversations_user" ON "ai_conversations" USING btree ("user_id","updated_at");--> statement-breakpoint +CREATE INDEX "idx_ai_messages_conversation" ON "ai_messages" USING btree ("conversation_id","created_at");--> statement-breakpoint +CREATE INDEX "idx_ai_proposals_user" ON "ai_proposals" USING btree ("user_id","status");--> statement-breakpoint +CREATE INDEX "idx_ai_proposals_conversation" ON "ai_proposals" USING btree ("conversation_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_ai_providers_user_label" ON "ai_providers" USING btree ("user_id","label");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_automation_channels_pair" ON "automation_channels" USING btree ("automation_id","channel_id");--> statement-breakpoint +CREATE INDEX "idx_automation_run_steps_run" ON "automation_run_steps" USING btree ("run_id","step_index");--> statement-breakpoint +CREATE INDEX "idx_automation_runs_automation" ON "automation_runs" USING btree ("automation_id","started_at");--> statement-breakpoint +CREATE INDEX "idx_automation_runs_user" ON "automation_runs" USING btree ("user_id","started_at");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_automation_schedules_automation" ON "automation_schedules" USING btree ("automation_id");--> statement-breakpoint +CREATE INDEX "idx_automation_schedules_due" ON "automation_schedules" USING btree ("next_due_at");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_automation_trigger_state_key" ON "automation_trigger_state" USING btree ("automation_id","state_key");--> statement-breakpoint +CREATE INDEX "idx_automations_user" ON "automations" USING btree ("user_id","enabled"); \ No newline at end of file diff --git a/drizzle/postgres/0013_open_swarm.sql b/drizzle/postgres/0013_open_swarm.sql new file mode 100644 index 0000000..c6f8a31 --- /dev/null +++ b/drizzle/postgres/0013_open_swarm.sql @@ -0,0 +1,2 @@ +ALTER TABLE "user_preferences" ADD COLUMN "terminal_defaults" text;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD COLUMN "rdp_defaults" text; \ No newline at end of file diff --git a/drizzle/postgres/0014_unusual_maelstrom.sql b/drizzle/postgres/0014_unusual_maelstrom.sql new file mode 100644 index 0000000..658d3b8 --- /dev/null +++ b/drizzle/postgres/0014_unusual_maelstrom.sql @@ -0,0 +1 @@ +ALTER TABLE "user_preferences" ADD COLUMN "terminal_macros" text; \ No newline at end of file diff --git a/drizzle/postgres/meta/0000_snapshot.json b/drizzle/postgres/meta/0000_snapshot.json new file mode 100644 index 0000000..3ace594 --- /dev/null +++ b/drizzle/postgres/meta/0000_snapshot.json @@ -0,0 +1,5778 @@ +{ + "id": "3d15ec30-87a9-4af4-855a-75e325a74c5d", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0001_snapshot.json b/drizzle/postgres/meta/0001_snapshot.json new file mode 100644 index 0000000..007a381 --- /dev/null +++ b/drizzle/postgres/meta/0001_snapshot.json @@ -0,0 +1,5836 @@ +{ + "id": "c1bfa827-464a-4704-be00-456c5bdaa1a8", + "prevId": "3d15ec30-87a9-4af4-855a-75e325a74c5d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0002_snapshot.json b/drizzle/postgres/meta/0002_snapshot.json new file mode 100644 index 0000000..ab437ce --- /dev/null +++ b/drizzle/postgres/meta/0002_snapshot.json @@ -0,0 +1,5895 @@ +{ + "id": "f7c2232a-3513-4c49-bbf4-b7afbaf1ac19", + "prevId": "c1bfa827-464a-4704-be00-456c5bdaa1a8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0003_snapshot.json b/drizzle/postgres/meta/0003_snapshot.json new file mode 100644 index 0000000..6aa2a30 --- /dev/null +++ b/drizzle/postgres/meta/0003_snapshot.json @@ -0,0 +1,5902 @@ +{ + "id": "cb4c2e4f-4af0-4400-8c42-19d867cf7943", + "prevId": "f7c2232a-3513-4c49-bbf4-b7afbaf1ac19", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0004_snapshot.json b/drizzle/postgres/meta/0004_snapshot.json new file mode 100644 index 0000000..150186f --- /dev/null +++ b/drizzle/postgres/meta/0004_snapshot.json @@ -0,0 +1,6091 @@ +{ + "id": "f518d6ad-56f7-4bf4-82d2-6f7ad9a72c5b", + "prevId": "cb4c2e4f-4af0-4400-8c42-19d867cf7943", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0005_snapshot.json b/drizzle/postgres/meta/0005_snapshot.json new file mode 100644 index 0000000..7b278b2 --- /dev/null +++ b/drizzle/postgres/meta/0005_snapshot.json @@ -0,0 +1,6098 @@ +{ + "id": "4435715f-2ccd-48f4-b0a7-7f8f44e6a125", + "prevId": "f518d6ad-56f7-4bf4-82d2-6f7ad9a72c5b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0006_snapshot.json b/drizzle/postgres/meta/0006_snapshot.json new file mode 100644 index 0000000..e66c435 --- /dev/null +++ b/drizzle/postgres/meta/0006_snapshot.json @@ -0,0 +1,6411 @@ +{ + "id": "58f37c2b-5c79-4b28-b7eb-aadc75561215", + "prevId": "4435715f-2ccd-48f4-b0a7-7f8f44e6a125", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0007_snapshot.json b/drizzle/postgres/meta/0007_snapshot.json new file mode 100644 index 0000000..5ce600e --- /dev/null +++ b/drizzle/postgres/meta/0007_snapshot.json @@ -0,0 +1,6430 @@ +{ + "id": "c4007e77-b1f2-468b-93eb-93a9a2462991", + "prevId": "58f37c2b-5c79-4b28-b7eb-aadc75561215", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0008_snapshot.json b/drizzle/postgres/meta/0008_snapshot.json new file mode 100644 index 0000000..e38eb4a --- /dev/null +++ b/drizzle/postgres/meta/0008_snapshot.json @@ -0,0 +1,6542 @@ +{ + "id": "5da91934-3c75-4ec0-a626-207b6af81f29", + "prevId": "c4007e77-b1f2-468b-93eb-93a9a2462991", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_workspaces": { + "name": "user_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0009_snapshot.json b/drizzle/postgres/meta/0009_snapshot.json new file mode 100644 index 0000000..d772145 --- /dev/null +++ b/drizzle/postgres/meta/0009_snapshot.json @@ -0,0 +1,6759 @@ +{ + "id": "cc725657-acdc-4449-9814-94fac6a334e1", + "prevId": "5da91934-3c75-4ec0-a626-207b6af81f29", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_workspaces": { + "name": "user_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0010_snapshot.json b/drizzle/postgres/meta/0010_snapshot.json new file mode 100644 index 0000000..25afe28 --- /dev/null +++ b/drizzle/postgres/meta/0010_snapshot.json @@ -0,0 +1,6805 @@ +{ + "id": "23ea2a99-7d72-4c02-8daf-708c04b62097", + "prevId": "cc725657-acdc-4449-9814-94fac6a334e1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_workspaces": { + "name": "user_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0011_snapshot.json b/drizzle/postgres/meta/0011_snapshot.json new file mode 100644 index 0000000..f377ad7 --- /dev/null +++ b/drizzle/postgres/meta/0011_snapshot.json @@ -0,0 +1,7302 @@ +{ + "id": "679ef592-a6fd-4c81-9889-89b0bcde7e19", + "prevId": "23ea2a99-7d72-4c02-8daf-708c04b62097", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + { + "expression": "snippet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_workspaces": { + "name": "user_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0012_snapshot.json b/drizzle/postgres/meta/0012_snapshot.json new file mode 100644 index 0000000..3060143 --- /dev/null +++ b/drizzle/postgres/meta/0012_snapshot.json @@ -0,0 +1,8444 @@ +{ + "id": "029820e0-269d-489c-a0db-641be03ea389", + "prevId": "679ef592-a6fd-4c81-9889-89b0bcde7e19", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_messages": { + "name": "ai_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_proposals": { + "name": "ai_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_channels": { + "name": "automation_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run_steps": { + "name": "automation_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_schedules": { + "name": "automation_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + { + "expression": "next_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_trigger_state": { + "name": "automation_trigger_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + { + "expression": "snippet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_workspaces": { + "name": "user_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0013_snapshot.json b/drizzle/postgres/meta/0013_snapshot.json new file mode 100644 index 0000000..e30099c --- /dev/null +++ b/drizzle/postgres/meta/0013_snapshot.json @@ -0,0 +1,8456 @@ +{ + "id": "d48c48e5-ea0c-4f17-9848-49445db3a0e6", + "prevId": "029820e0-269d-489c-a0db-641be03ea389", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_messages": { + "name": "ai_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_proposals": { + "name": "ai_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_channels": { + "name": "automation_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run_steps": { + "name": "automation_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_schedules": { + "name": "automation_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + { + "expression": "next_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_trigger_state": { + "name": "automation_trigger_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + { + "expression": "snippet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_workspaces": { + "name": "user_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0014_snapshot.json b/drizzle/postgres/meta/0014_snapshot.json new file mode 100644 index 0000000..dc7101d --- /dev/null +++ b/drizzle/postgres/meta/0014_snapshot.json @@ -0,0 +1,8462 @@ +{ + "id": "63268872-e33c-4e4c-ae9d-bd5c7340bf8b", + "prevId": "d48c48e5-ea0c-4f17-9848-49445db3a0e6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_messages": { + "name": "ai_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_proposals": { + "name": "ai_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_firings": { + "name": "alert_firings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_channels": { + "name": "alert_rule_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_channels": { + "name": "automation_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run_steps": { + "name": "automation_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_schedules": { + "name": "automation_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + { + "expression": "next_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_trigger_state": { + "name": "automation_trigger_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.command_history": { + "name": "command_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_pinned": { + "name": "file_manager_pinned", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_recent": { + "name": "file_manager_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_inventory": { + "name": "fleet_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleet_members": { + "name": "fleet_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + { + "expression": "fleet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fleets": { + "name": "fleets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_topology": { + "name": "network_topology", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_node_history": { + "name": "proxmox_node_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_access": { + "name": "snippet_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + { + "expression": "snippet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippet_folders": { + "name": "snippet_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.snippets": { + "name": "snippets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_note": { + "name": "is_note", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transfer_recent": { + "name": "transfer_recent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_workspaces": { + "name": "user_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json new file mode 100644 index 0000000..ed3a208 --- /dev/null +++ b/drizzle/postgres/meta/_journal.json @@ -0,0 +1,111 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1785738871078, + "tag": "0000_jazzy_infant_terrible", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1786132418140, + "tag": "0001_worried_silvermane", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1786147318741, + "tag": "0002_clear_cerebro", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1786258963173, + "tag": "0003_harsh_gravity", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1786423595034, + "tag": "0004_great_victor_mancha", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1786428084849, + "tag": "0005_loose_captain_marvel", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1786482020978, + "tag": "0006_gigantic_thor_girl", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1786487750371, + "tag": "0007_orange_mandrill", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1786498679719, + "tag": "0008_bright_miss_america", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1786509724258, + "tag": "0009_spicy_the_leader", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1786515117043, + "tag": "0010_nasty_mordo", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1786519423735, + "tag": "0011_unique_sleepwalker", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1786598522041, + "tag": "0012_dashing_mandroid", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1786737723263, + "tag": "0013_open_swarm", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1786757021444, + "tag": "0014_unusual_maelstrom", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/sqlite/0000_stormy_veda.sql b/drizzle/sqlite/0000_stormy_veda.sql new file mode 100644 index 0000000..e1f4bed --- /dev/null +++ b/drizzle/sqlite/0000_stormy_veda.sql @@ -0,0 +1,855 @@ +CREATE TABLE `alert_firings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `rule_id` integer NOT NULL, + `host_id` integer NOT NULL, + `host_name` text NOT NULL, + `fired_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `resolved_at` text, + `value` real, + `message` text NOT NULL, + `severity` text DEFAULT 'warning' NOT NULL, + `acknowledged` integer DEFAULT false NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`rule_id`) REFERENCES `alert_rules`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `alert_rule_channels` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `rule_id` integer NOT NULL, + `channel_id` integer NOT NULL, + FOREIGN KEY (`rule_id`) REFERENCES `alert_rules`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`channel_id`) REFERENCES `notification_channels`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `alert_rules` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer, + `name` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `trigger_type` text NOT NULL, + `threshold_value` real, + `threshold_duration_seconds` integer, + `cooldown_minutes` integer DEFAULT 15 NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `api_keys` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `token_hash` text NOT NULL, + `token_prefix` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `expires_at` text, + `last_used_at` text, + `is_active` integer DEFAULT true NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `audit_logs` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text, + `username` text NOT NULL, + `action` text NOT NULL, + `resource_type` text NOT NULL, + `resource_id` text, + `resource_name` text, + `details` text, + `ip_address` text, + `user_agent` text, + `success` integer NOT NULL, + `error_message` text, + `timestamp` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `c2s_tunnel_presets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `config` text NOT NULL, + `platform` text, + `computer_name` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `command_history` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `command` text NOT NULL, + `executed_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `credential_sidebar_preferences` ( + `user_id` text PRIMARY KEY NOT NULL, + `data` text NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `dashboard_service_links` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `label` text NOT NULL, + `url` text NOT NULL, + `order` integer DEFAULT 0 NOT NULL, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `dashboard_service_links_sync_id_unique` ON `dashboard_service_links` (`sync_id`);--> statement-breakpoint +CREATE TABLE `dismissed_alerts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `alert_id` text NOT NULL, + `dismissed_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `file_manager_pinned` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `name` text NOT NULL, + `path` text NOT NULL, + `pinned_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `file_manager_recent` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `name` text NOT NULL, + `path` text NOT NULL, + `last_opened` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `file_manager_shortcuts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `name` text NOT NULL, + `path` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `homepage_items` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `type_id` text NOT NULL, + `title` text, + `config` text DEFAULT '{}' NOT NULL, + `folder_id` integer, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `homepage_items_sync_id_unique` ON `homepage_items` (`sync_id`);--> statement-breakpoint +CREATE TABLE `homepage_layouts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `layout` text DEFAULT '{}' NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `homepage_layouts_user_id_unique` ON `homepage_layouts` (`user_id`);--> statement-breakpoint +CREATE TABLE `host_access` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `host_id` integer NOT NULL, + `user_id` text, + `role_id` integer, + `granted_by` text NOT NULL, + `permission_level` text DEFAULT 'connect' NOT NULL, + `expires_at` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `last_accessed_at` text, + `access_count` integer DEFAULT 0 NOT NULL, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`granted_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `host_health_checks` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `checks` text NOT NULL, + `interval_seconds` integer DEFAULT 300 NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_host_health_checks_user_host` ON `host_health_checks` (`user_id`,`host_id`);--> statement-breakpoint +CREATE TABLE `host_health_history` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `check_id` text NOT NULL, + `ts` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `ok` integer NOT NULL, + `latency_ms` integer, + `detail` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `host_metrics_history` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `host_id` integer NOT NULL, + `ts` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `cpu_percent` real, + `mem_percent` real, + `disk_percent` real, + `net_rx_bytes` integer, + `net_tx_bytes` integer, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `host_metrics_preferences` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `layout` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_host_metrics_prefs_user_host` ON `host_metrics_preferences` (`user_id`,`host_id`);--> statement-breakpoint +CREATE TABLE `host_sidebar_preferences` ( + `user_id` text PRIMARY KEY NOT NULL, + `data` text NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `ssh_data` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `connection_type` text DEFAULT 'ssh' NOT NULL, + `name` text, + `ip` text NOT NULL, + `port` integer NOT NULL, + `username` text NOT NULL, + `folder` text, + `tags` text, + `pin` integer DEFAULT false NOT NULL, + `sort_order` integer, + `auth_type` text NOT NULL, + `use_warpgate` integer DEFAULT false NOT NULL, + `share_ssh_auth` integer DEFAULT false NOT NULL, + `force_keyboard_interactive` text, + `password` text, + `key` text(8192), + `key_password` text, + `key_type` text, + `sudo_password` text, + `autostart_password` text, + `autostart_key` text(8192), + `autostart_key_password` text, + `credential_id` integer, + `override_credential_username` integer, + `vault_profile_id` integer, + `enable_terminal` integer DEFAULT true NOT NULL, + `enable_session_logging` integer DEFAULT true NOT NULL, + `allow_session_sharing` integer DEFAULT true NOT NULL, + `enable_command_history` integer DEFAULT true NOT NULL, + `enable_tunnel` integer DEFAULT true NOT NULL, + `tunnel_connections` text, + `jump_hosts` text, + `enable_file_manager` integer DEFAULT true NOT NULL, + `scp_legacy` integer DEFAULT false NOT NULL, + `enable_docker` integer DEFAULT false NOT NULL, + `enable_tmux_monitor` integer DEFAULT false NOT NULL, + `show_terminal_in_sidebar` integer DEFAULT true NOT NULL, + `show_file_manager_in_sidebar` integer DEFAULT false NOT NULL, + `show_tunnel_in_sidebar` integer DEFAULT false NOT NULL, + `show_docker_in_sidebar` integer DEFAULT false NOT NULL, + `show_server_stats_in_sidebar` integer DEFAULT false NOT NULL, + `default_path` text, + `stats_config` text, + `docker_config` text, + `enable_proxmox` integer DEFAULT false NOT NULL, + `proxmox_config` text, + `terminal_config` text, + `quick_actions` text, + `notes` text, + `enable_ssh` integer DEFAULT true NOT NULL, + `enable_rdp` integer DEFAULT false NOT NULL, + `enable_vnc` integer DEFAULT false NOT NULL, + `enable_telnet` integer DEFAULT false NOT NULL, + `ssh_port` integer DEFAULT 22, + `rdp_port` integer DEFAULT 3389, + `vnc_port` integer DEFAULT 5900, + `telnet_port` integer DEFAULT 23, + `rdp_credential_id` integer, + `rdp_user` text, + `rdp_password` text, + `rdp_domain` text, + `rdp_security` text, + `rdp_ignore_cert` integer DEFAULT false, + `vnc_credential_id` integer, + `vnc_password` text, + `vnc_user` text, + `telnet_user` text, + `telnet_password` text, + `telnet_credential_id` integer, + `rdp_auth_type` text, + `vnc_auth_type` text, + `telnet_auth_type` text, + `domain` text, + `security` text, + `ignore_cert` integer DEFAULT false, + `guacamole_config` text, + `use_socks5` integer, + `socks5_host` text, + `socks5_port` integer, + `socks5_username` text, + `socks5_password` text, + `socks5_proxy_chain` text, + `connection_origin` text, + `mac_address` text, + `wol_broadcast_address` text, + `port_knock_sequence` text, + `host_key_fingerprint` text, + `host_key_type` text, + `host_key_algorithm` text DEFAULT 'sha256', + `host_key_first_seen` text, + `host_key_last_verified` text, + `host_key_changed_count` integer DEFAULT 0, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`vault_profile_id`) REFERENCES `vault_profiles`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`rdp_credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`vnc_credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`telnet_credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `ssh_data_sync_id_unique` ON `ssh_data` (`sync_id`);--> statement-breakpoint +CREATE TABLE `network_topology` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `topology` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `notification_channels` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `config` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `opkssh_tokens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `ssh_cert` text(8192) NOT NULL, + `private_key` text(8192) NOT NULL, + `email` text, + `sub` text, + `issuer` text, + `audience` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `expires_at` text NOT NULL, + `last_used` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_opkssh_tokens_user_host` ON `opkssh_tokens` (`user_id`,`host_id`);--> statement-breakpoint +CREATE TABLE `recent_activity` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `type` text NOT NULL, + `host_id` integer NOT NULL, + `host_name` text, + `timestamp` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `display_name` text NOT NULL, + `description` text, + `is_system` integer DEFAULT false NOT NULL, + `permissions` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `roles_name_unique` ON `roles` (`name`);--> statement-breakpoint +CREATE TABLE `session_recordings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `host_id` integer NOT NULL, + `user_id` text, + `username` text, + `access_id` integer, + `started_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `ended_at` text, + `duration` integer, + `commands` text, + `dangerous_actions` text, + `recording_path` text, + `protocol` text DEFAULT 'ssh' NOT NULL, + `format` text DEFAULT 'text' NOT NULL, + `terminated_by_owner` integer DEFAULT false, + `termination_reason` text, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`access_id`) REFERENCES `host_access`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `session_share_participants` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `share_id` text NOT NULL, + `user_id` text, + `guest_label` text, + `joined_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `left_at` text, + FOREIGN KEY (`share_id`) REFERENCES `session_shares`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `session_shares` ( + `id` text PRIMARY KEY NOT NULL, + `host_id` integer NOT NULL, + `owner_user_id` text NOT NULL, + `protocol` text NOT NULL, + `session_id` text NOT NULL, + `tab_instance_id` text, + `share_type` text NOT NULL, + `target_user_id` text, + `link_token` text, + `permission_level` text DEFAULT 'read-only' NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `expires_at` text NOT NULL, + `revoked_at` text, + `last_joined_at` text, + `join_count` integer DEFAULT 0 NOT NULL, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`owner_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`target_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_shares_link_token_unique` ON `session_shares` (`link_token`);--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `jwt_token` text NOT NULL, + `device_type` text NOT NULL, + `device_info` text NOT NULL, + `oidc_sub` text, + `oidc_sid` text, + `sso_provider_id` integer, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `expires_at` text NOT NULL, + `last_active_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `settings` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `shared_host_auth_overrides` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `host_id` integer NOT NULL, + `user_id` text NOT NULL, + `protocol` text DEFAULT 'ssh' NOT NULL, + `credential_id` integer NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `shared_host_auth_overrides_host_user_protocol_unique` ON `shared_host_auth_overrides` (`host_id`,`user_id`,`protocol`);--> statement-breakpoint +CREATE TABLE `shared_host_secrets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `host_access_id` integer NOT NULL, + `target_user_id` text NOT NULL, + `protocol` text DEFAULT 'ssh' NOT NULL, + `source_type` text DEFAULT 'credential' NOT NULL, + `original_credential_id` integer, + `encrypted_username` text, + `encrypted_auth_type` text, + `encrypted_password` text, + `encrypted_key` text(16384), + `encrypted_key_password` text, + `encrypted_key_type` text, + `encrypted_domain` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`host_access_id`) REFERENCES `host_access`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`target_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`original_credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_shared_host_secrets_scope` ON `shared_host_secrets` (`host_access_id`,`target_user_id`,`protocol`);--> statement-breakpoint +CREATE TABLE `snippet_access` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `snippet_id` integer NOT NULL, + `user_id` text, + `role_id` integer, + `granted_by` text NOT NULL, + `permission_level` text DEFAULT 'view' NOT NULL, + `expires_at` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`snippet_id`) REFERENCES `snippets`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`granted_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `snippet_folders` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `color` text, + `icon` text, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `snippet_folders_sync_id_unique` ON `snippet_folders` (`sync_id`);--> statement-breakpoint +CREATE TABLE `snippets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `content` text NOT NULL, + `description` text, + `folder` text, + `order` integer DEFAULT 0 NOT NULL, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `host_filter` text, + `is_note` integer DEFAULT false NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `snippets_sync_id_unique` ON `snippets` (`sync_id`);--> statement-breakpoint +CREATE TABLE `ssh_credential_usage` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `credential_id` integer NOT NULL, + `host_id` integer NOT NULL, + `user_id` text NOT NULL, + `used_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `ssh_credentials` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `description` text, + `folder` text, + `tags` text, + `pin` integer DEFAULT false NOT NULL, + `sort_order` integer, + `auth_type` text NOT NULL, + `username` text, + `password` text, + `key` text(16384), + `private_key` text(16384), + `public_key` text(4096), + `key_password` text, + `key_type` text, + `detected_key_type` text, + `cert_public_key` text(8192), + `usage_count` integer DEFAULT 0 NOT NULL, + `last_used` text, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `ssh_credentials_sync_id_unique` ON `ssh_credentials` (`sync_id`);--> statement-breakpoint +CREATE TABLE `ssh_folders` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `color` text, + `icon` text, + `credential_id` integer, + `sort_order` integer, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `ssh_folders_sync_id_unique` ON `ssh_folders` (`sync_id`);--> statement-breakpoint +CREATE TABLE `sso_providers` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `display_order` integer DEFAULT 0 NOT NULL, + `config` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE `sync_tombstones` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `entity_type` text NOT NULL, + `sync_id` text NOT NULL, + `deleted_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `termix_identities` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `handle` text NOT NULL, + `description` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `termix_identities_user_id_unique` ON `termix_identities` (`user_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `termix_identities_handle_unique` ON `termix_identities` (`handle`);--> statement-breakpoint +CREATE TABLE `termix_identity_ca` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `identity_id` integer NOT NULL, + `user_id` text NOT NULL, + `public_key` text(4096) NOT NULL, + `private_key` text(8192) NOT NULL, + `validity_days` integer DEFAULT 90 NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`identity_id`) REFERENCES `termix_identities`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `termix_identity_ca_identity_id_unique` ON `termix_identity_ca` (`identity_id`);--> statement-breakpoint +CREATE TABLE `termix_identity_keys` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `identity_id` integer NOT NULL, + `user_id` text NOT NULL, + `public_key` text(8192) NOT NULL, + `key_type` text NOT NULL, + `algorithm` text NOT NULL, + `label` text, + `comment` text, + `source` text DEFAULT 'manual' NOT NULL, + `credential_id` integer, + `enabled` integer DEFAULT true NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`identity_id`) REFERENCES `termix_identities`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`credential_id`) REFERENCES `ssh_credentials`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `tmux_session_tags` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `session_name` text NOT NULL, + `tag` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `transfer_recent` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `source_host_id` integer NOT NULL, + `dest_host_id` integer NOT NULL, + `dest_path` text NOT NULL, + `dest_path_label` text NOT NULL, + `last_used` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`source_host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`dest_host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `trusted_devices` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `device_fingerprint` text NOT NULL, + `device_type` text NOT NULL, + `device_info` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `expires_at` text NOT NULL, + `last_used_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `user_open_tabs` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `tab_type` text NOT NULL, + `host_id` integer, + `label` text NOT NULL, + `tab_order` integer DEFAULT 0 NOT NULL, + `backend_session_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `user_preferences` ( + `user_id` text PRIMARY KEY NOT NULL, + `reopen_tabs_on_login` integer DEFAULT false NOT NULL, + `theme` text, + `font_size` text, + `accent_color` text, + `language` text, + `storage_mode` text, + `command_autocomplete` integer, + `command_palette_enabled` integer, + `show_host_tags` integer, + `host_tray_on_click` integer, + `pin_app_rail` integer, + `expand_app_rail_on_hover` integer, + `folders_collapsed` integer, + `confirm_snippet_execution` integer, + `disable_update_check` integer, + `confirm_tab_close` integer, + `hidden_rail_tabs` text, + `compact_host_view` integer, + `status_color_scheme` text, + `custom_themes` text, + `custom_keybindings` text, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `user_roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `role_id` integer NOT NULL, + `granted_by` text, + `granted_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`granted_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_user_roles_user_role` ON `user_roles` (`user_id`,`role_id`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `username` text NOT NULL, + `password_hash` text NOT NULL, + `is_admin` integer DEFAULT false NOT NULL, + `is_oidc` integer DEFAULT false NOT NULL, + `oidc_identifier` text, + `sso_provider_id` integer, + `client_id` text, + `client_secret` text, + `issuer_url` text, + `authorization_url` text, + `token_url` text, + `identifier_path` text, + `name_path` text, + `scopes` text DEFAULT 'openid email profile', + `totp_secret` text, + `totp_enabled` integer DEFAULT false NOT NULL, + `totp_backup_codes` text, + `registered_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `donation_modal_dismissed` integer DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE `vault_profiles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `description` text, + `folder` text, + `tags` text, + `vault_addr` text NOT NULL, + `vault_namespace` text, + `oidc_mount` text, + `oidc_role` text, + `ssh_mount` text, + `ssh_role` text NOT NULL, + `valid_principals` text, + `key_type` text, + `shared` integer DEFAULT false NOT NULL, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `vault_profiles_sync_id_unique` ON `vault_profiles` (`sync_id`);--> statement-breakpoint +CREATE TABLE `vault_tokens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `profile_id` integer NOT NULL, + `ssh_cert` text(8192) NOT NULL, + `private_key` text(8192) NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `expires_at` text NOT NULL, + `last_used` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`profile_id`) REFERENCES `vault_profiles`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_vault_tokens_user_profile` ON `vault_tokens` (`user_id`,`profile_id`);--> statement-breakpoint +CREATE TABLE `webauthn_credentials` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `credential_id` text NOT NULL, + `public_key` text NOT NULL, + `counter` integer DEFAULT 0 NOT NULL, + `device_type` text, + `backed_up` integer DEFAULT false NOT NULL, + `transports` text, + `user_verification` text DEFAULT 'preferred' NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `last_used_at` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/drizzle/sqlite/0001_colorful_the_call.sql b/drizzle/sqlite/0001_colorful_the_call.sql new file mode 100644 index 0000000..e065384 --- /dev/null +++ b/drizzle/sqlite/0001_colorful_the_call.sql @@ -0,0 +1,26 @@ +CREATE TABLE `proxmox_node_history` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `host_id` integer NOT NULL, + `ts` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `cpu_percent` real, + `mem_percent` real, + `disk_percent` real, + `net_rx_bytes` integer, + `net_tx_bytes` integer, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `proxmox_stats_preferences` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `host_id` integer NOT NULL, + `layout` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_proxmox_stats_prefs_user_host` ON `proxmox_stats_preferences` (`user_id`,`host_id`);--> statement-breakpoint +ALTER TABLE `ssh_data` ADD `enable_proxmox_stats` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD `proxmox_stats_config` text; \ No newline at end of file diff --git a/drizzle/sqlite/0002_woozy_turbo.sql b/drizzle/sqlite/0002_woozy_turbo.sql new file mode 100644 index 0000000..561880e --- /dev/null +++ b/drizzle/sqlite/0002_woozy_turbo.sql @@ -0,0 +1 @@ +ALTER TABLE `ssh_data` ADD `enable_terminal_toolbar` integer DEFAULT true NOT NULL; \ No newline at end of file diff --git a/drizzle/sqlite/0003_premium_ultimates.sql b/drizzle/sqlite/0003_premium_ultimates.sql new file mode 100644 index 0000000..d25865b --- /dev/null +++ b/drizzle/sqlite/0003_premium_ultimates.sql @@ -0,0 +1,42 @@ +CREATE TABLE `fleet_inventory` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `host_id` integer NOT NULL, + `user_id` text NOT NULL, + `os_pretty_name` text, + `kernel` text, + `architecture` text, + `hostname` text, + `uptime_seconds` integer, + `ip` text, + `package_manager` text, + `collected_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_fleet_inventory_host` ON `fleet_inventory` (`host_id`,`user_id`);--> statement-breakpoint +CREATE TABLE `fleet_members` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `fleet_id` integer NOT NULL, + `host_id` integer NOT NULL, + `added_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`fleet_id`) REFERENCES `fleets`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_fleet_members_fleet_host` ON `fleet_members` (`fleet_id`,`host_id`);--> statement-breakpoint +CREATE TABLE `fleets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `description` text, + `color` text, + `icon` text, + `tag_rules` text, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `fleets_sync_id_unique` ON `fleets` (`sync_id`); \ No newline at end of file diff --git a/drizzle/sqlite/0004_cool_zuras.sql b/drizzle/sqlite/0004_cool_zuras.sql new file mode 100644 index 0000000..4332860 --- /dev/null +++ b/drizzle/sqlite/0004_cool_zuras.sql @@ -0,0 +1 @@ +ALTER TABLE `ssh_data` ADD `parent_host_id` integer REFERENCES ssh_data(id) ON DELETE SET NULL; \ No newline at end of file diff --git a/drizzle/sqlite/0005_thin_sentry.sql b/drizzle/sqlite/0005_thin_sentry.sql new file mode 100644 index 0000000..ef960ee --- /dev/null +++ b/drizzle/sqlite/0005_thin_sentry.sql @@ -0,0 +1,17 @@ +CREATE TABLE `user_workspaces` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `color` text, + `icon` text, + `kind` text DEFAULT 'manual' NOT NULL, + `is_default` integer DEFAULT false NOT NULL, + `payload` text DEFAULT '{}' NOT NULL, + `sync_id` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `last_used_at` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_workspaces_sync_id_unique` ON `user_workspaces` (`sync_id`); \ No newline at end of file diff --git a/drizzle/sqlite/0006_nebulous_demogoblin.sql b/drizzle/sqlite/0006_nebulous_demogoblin.sql new file mode 100644 index 0000000..9f8c641 --- /dev/null +++ b/drizzle/sqlite/0006_nebulous_demogoblin.sql @@ -0,0 +1,6 @@ +CREATE TABLE `ui_preferences` ( + `user_id` text PRIMARY KEY NOT NULL, + `data` text NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/drizzle/sqlite/0007_complex_nebula.sql b/drizzle/sqlite/0007_complex_nebula.sql new file mode 100644 index 0000000..a312d6b --- /dev/null +++ b/drizzle/sqlite/0007_complex_nebula.sql @@ -0,0 +1,42 @@ +CREATE INDEX `idx_alert_firings_rule` ON `alert_firings` (`rule_id`,`fired_at`);--> statement-breakpoint +CREATE INDEX `idx_alert_firings_host` ON `alert_firings` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_api_keys_user_id` ON `api_keys` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_audit_logs_timestamp` ON `audit_logs` (`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_audit_logs_user_ts` ON `audit_logs` (`user_id`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_audit_logs_action_ts` ON `audit_logs` (`action`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_audit_logs_resource_ts` ON `audit_logs` (`resource_type`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_command_history_user_host` ON `command_history` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_dismissed_alerts_user_id` ON `dismissed_alerts` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_file_manager_pinned_user` ON `file_manager_pinned` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_file_manager_recent_user` ON `file_manager_recent` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_file_manager_shortcuts_user` ON `file_manager_shortcuts` (`user_id`,`host_id`);--> statement-breakpoint +CREATE INDEX `idx_fleet_inventory_user` ON `fleet_inventory` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_fleet_members_host` ON `fleet_members` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_homepage_items_user_id` ON `homepage_items` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_host_access_user_id` ON `host_access` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_host_access_role_id` ON `host_access` (`role_id`);--> statement-breakpoint +CREATE INDEX `idx_host_access_host_id` ON `host_access` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_host_access_expires_at` ON `host_access` (`expires_at`);--> statement-breakpoint +CREATE INDEX `idx_ssh_data_user_id` ON `ssh_data` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_data_parent_host` ON `ssh_data` (`parent_host_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_data_credential` ON `ssh_data` (`credential_id`);--> statement-breakpoint +CREATE INDEX `idx_recent_activity_user_ts` ON `recent_activity` (`user_id`,`timestamp`);--> statement-breakpoint +CREATE INDEX `idx_session_recordings_user_started` ON `session_recordings` (`user_id`,`started_at`);--> statement-breakpoint +CREATE INDEX `idx_session_recordings_host` ON `session_recordings` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_session_shares_session_id` ON `session_shares` (`session_id`);--> statement-breakpoint +CREATE INDEX `idx_session_shares_host_id` ON `session_shares` (`host_id`);--> statement-breakpoint +CREATE INDEX `idx_sessions_user_id` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_sessions_expires_at` ON `sessions` (`expires_at`);--> statement-breakpoint +CREATE INDEX `idx_snippet_access_user_id` ON `snippet_access` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_snippet_access_snippet_id` ON `snippet_access` (`snippet_id`);--> statement-breakpoint +CREATE INDEX `idx_snippet_access_role_id` ON `snippet_access` (`role_id`);--> statement-breakpoint +CREATE INDEX `idx_snippets_user_id` ON `snippets` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_credential_usage_credential` ON `ssh_credential_usage` (`credential_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_credential_usage_user` ON `ssh_credential_usage` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_credentials_user_id` ON `ssh_credentials` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_folders_user_id` ON `ssh_folders` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_transfer_recent_user` ON `transfer_recent` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_trusted_devices_user_id` ON `trusted_devices` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_user_open_tabs_user_id` ON `user_open_tabs` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_user_roles_role_id` ON `user_roles` (`role_id`);--> statement-breakpoint +CREATE INDEX `idx_user_workspaces_user_id` ON `user_workspaces` (`user_id`); \ No newline at end of file diff --git a/drizzle/sqlite/0008_fast_imperial_guard.sql b/drizzle/sqlite/0008_fast_imperial_guard.sql new file mode 100644 index 0000000..883b749 --- /dev/null +++ b/drizzle/sqlite/0008_fast_imperial_guard.sql @@ -0,0 +1,147 @@ +CREATE TABLE `ai_conversations` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `title` text, + `provider_id` integer, + `model` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `idx_ai_conversations_user` ON `ai_conversations` (`user_id`,`updated_at`);--> statement-breakpoint +CREATE TABLE `ai_messages` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `conversation_id` integer NOT NULL, + `role` text NOT NULL, + `content` text DEFAULT '' NOT NULL, + `tool_calls` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`conversation_id`) REFERENCES `ai_conversations`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `idx_ai_messages_conversation` ON `ai_messages` (`conversation_id`,`created_at`);--> statement-breakpoint +CREATE TABLE `ai_proposals` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `conversation_id` integer NOT NULL, + `user_id` text NOT NULL, + `kind` text NOT NULL, + `summary` text, + `payload` text DEFAULT '{}' NOT NULL, + `status` text DEFAULT 'pending' NOT NULL, + `applied_at` text, + `result_summary` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`conversation_id`) REFERENCES `ai_conversations`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `idx_ai_proposals_user` ON `ai_proposals` (`user_id`,`status`);--> statement-breakpoint +CREATE INDEX `idx_ai_proposals_conversation` ON `ai_proposals` (`conversation_id`);--> statement-breakpoint +CREATE TABLE `ai_providers` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `provider_type` text NOT NULL, + `label` text NOT NULL, + `base_url` text, + `api_key` text(8192), + `api_key_prefix` text, + `default_model` text, + `enabled` integer DEFAULT true NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_ai_providers_user_label` ON `ai_providers` (`user_id`,`label`);--> statement-breakpoint +CREATE TABLE `automation_channels` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `automation_id` integer NOT NULL, + `channel_id` integer NOT NULL, + FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`channel_id`) REFERENCES `notification_channels`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_automation_channels_pair` ON `automation_channels` (`automation_id`,`channel_id`);--> statement-breakpoint +CREATE TABLE `automation_run_steps` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `run_id` integer NOT NULL, + `step_index` integer NOT NULL, + `step_id` text NOT NULL, + `step_type` text NOT NULL, + `status` text NOT NULL, + `started_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `finished_at` text, + `output` text, + `error` text, + `truncated` integer DEFAULT false NOT NULL, + FOREIGN KEY (`run_id`) REFERENCES `automation_runs`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `idx_automation_run_steps_run` ON `automation_run_steps` (`run_id`,`step_index`);--> statement-breakpoint +CREATE TABLE `automation_runs` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `automation_id` integer NOT NULL, + `user_id` text NOT NULL, + `trigger_type` text NOT NULL, + `trigger_context` text, + `status` text NOT NULL, + `started_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `finished_at` text, + `duration_ms` integer, + `error` text, + `dry_run` integer DEFAULT false NOT NULL, + `parent_run_id` integer, + FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `idx_automation_runs_automation` ON `automation_runs` (`automation_id`,`started_at`);--> statement-breakpoint +CREATE INDEX `idx_automation_runs_user` ON `automation_runs` (`user_id`,`started_at`);--> statement-breakpoint +CREATE TABLE `automation_schedules` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `automation_id` integer NOT NULL, + `cron` text, + `interval_seconds` integer, + `timezone` text, + `next_due_at` text, + `last_tick_at` text, + FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_automation_schedules_automation` ON `automation_schedules` (`automation_id`);--> statement-breakpoint +CREATE INDEX `idx_automation_schedules_due` ON `automation_schedules` (`next_due_at`);--> statement-breakpoint +CREATE TABLE `automation_trigger_state` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `automation_id` integer NOT NULL, + `state_key` text NOT NULL, + `breach_started_at` text, + `last_fired_at` text, + `last_value` real, + `last_observed_state` text, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`automation_id`) REFERENCES `automations`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `idx_automation_trigger_state_key` ON `automation_trigger_state` (`automation_id`,`state_key`);--> statement-breakpoint +CREATE TABLE `automations` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `description` text, + `enabled` integer DEFAULT true NOT NULL, + `definition` text NOT NULL, + `definition_version` integer DEFAULT 1 NOT NULL, + `concurrency_policy` text DEFAULT 'skip' NOT NULL, + `max_run_seconds` integer DEFAULT 300 NOT NULL, + `dry_run` integer DEFAULT false NOT NULL, + `last_run_at` text, + `last_run_status` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `idx_automations_user` ON `automations` (`user_id`,`enabled`);--> statement-breakpoint +ALTER TABLE `user_preferences` ADD `ai_assistant_enabled` integer;--> statement-breakpoint +ALTER TABLE `user_preferences` ADD `ai_read_only_commands` integer; \ No newline at end of file diff --git a/drizzle/sqlite/0009_pink_susan_delgado.sql b/drizzle/sqlite/0009_pink_susan_delgado.sql new file mode 100644 index 0000000..0ebb111 --- /dev/null +++ b/drizzle/sqlite/0009_pink_susan_delgado.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user_preferences` ADD `terminal_defaults` text;--> statement-breakpoint +ALTER TABLE `user_preferences` ADD `rdp_defaults` text; \ No newline at end of file diff --git a/drizzle/sqlite/0010_mean_queen_noir.sql b/drizzle/sqlite/0010_mean_queen_noir.sql new file mode 100644 index 0000000..df84506 --- /dev/null +++ b/drizzle/sqlite/0010_mean_queen_noir.sql @@ -0,0 +1 @@ +ALTER TABLE `user_preferences` ADD `terminal_macros` text; \ No newline at end of file diff --git a/drizzle/sqlite/meta/0000_snapshot.json b/drizzle/sqlite/meta/0000_snapshot.json new file mode 100644 index 0000000..ff66a79 --- /dev/null +++ b/drizzle/sqlite/meta/0000_snapshot.json @@ -0,0 +1,6209 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "818cd7d9-d223-494f-ac73-f98708c72dc2", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/sqlite/meta/0001_snapshot.json b/drizzle/sqlite/meta/0001_snapshot.json new file mode 100644 index 0000000..6f9b2c1 --- /dev/null +++ b/drizzle/sqlite/meta/0001_snapshot.json @@ -0,0 +1,6395 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "773098a5-7c1c-4cc7-aaa7-33494b6578df", + "prevId": "818cd7d9-d223-494f-ac73-f98708c72dc2", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0002_snapshot.json b/drizzle/sqlite/meta/0002_snapshot.json new file mode 100644 index 0000000..1998f5e --- /dev/null +++ b/drizzle/sqlite/meta/0002_snapshot.json @@ -0,0 +1,6403 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "741b6f3f-37a3-42a5-ba62-6e3d2127e031", + "prevId": "773098a5-7c1c-4cc7-aaa7-33494b6578df", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0003_snapshot.json b/drizzle/sqlite/meta/0003_snapshot.json new file mode 100644 index 0000000..fa57c50 --- /dev/null +++ b/drizzle/sqlite/meta/0003_snapshot.json @@ -0,0 +1,6706 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7024c984-caee-4ce9-a6f3-27907eb0143d", + "prevId": "741b6f3f-37a3-42a5-ba62-6e3d2127e031", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0004_snapshot.json b/drizzle/sqlite/meta/0004_snapshot.json new file mode 100644 index 0000000..3d62f18 --- /dev/null +++ b/drizzle/sqlite/meta/0004_snapshot.json @@ -0,0 +1,6726 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "333b4446-ead3-418f-9abe-6958b5164cbc", + "prevId": "7024c984-caee-4ce9-a6f3-27907eb0143d", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0005_snapshot.json b/drizzle/sqlite/meta/0005_snapshot.json new file mode 100644 index 0000000..dfb8619 --- /dev/null +++ b/drizzle/sqlite/meta/0005_snapshot.json @@ -0,0 +1,6847 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "94b5c3da-f963-4c0f-8446-c0e69438c696", + "prevId": "333b4446-ead3-418f-9abe-6958b5164cbc", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0006_snapshot.json b/drizzle/sqlite/meta/0006_snapshot.json new file mode 100644 index 0000000..c9aaa5f --- /dev/null +++ b/drizzle/sqlite/meta/0006_snapshot.json @@ -0,0 +1,6893 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2ecae16a-8588-4a55-8d8b-015ef61eeebe", + "prevId": "94b5c3da-f963-4c0f-8446-c0e69438c696", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/sqlite/meta/0007_snapshot.json b/drizzle/sqlite/meta/0007_snapshot.json new file mode 100644 index 0000000..9055472 --- /dev/null +++ b/drizzle/sqlite/meta/0007_snapshot.json @@ -0,0 +1,7214 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "00ef816a-f782-4f84-9939-0920df15cbc8", + "prevId": "2ecae16a-8588-4a55-8d8b-015ef61eeebe", + "tables": { + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + }, + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0008_snapshot.json b/drizzle/sqlite/meta/0008_snapshot.json new file mode 100644 index 0000000..a826353 --- /dev/null +++ b/drizzle/sqlite/meta/0008_snapshot.json @@ -0,0 +1,8263 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5d2a8047-80bc-422e-8ae0-0076848e4c1e", + "prevId": "00ef816a-f782-4f84-9939-0920df15cbc8", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + }, + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0009_snapshot.json b/drizzle/sqlite/meta/0009_snapshot.json new file mode 100644 index 0000000..d2ebf55 --- /dev/null +++ b/drizzle/sqlite/meta/0009_snapshot.json @@ -0,0 +1,8277 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "54b875dd-78fc-4ae4-a00a-a6594bb31714", + "prevId": "5d2a8047-80bc-422e-8ae0-0076848e4c1e", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + }, + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0010_snapshot.json b/drizzle/sqlite/meta/0010_snapshot.json new file mode 100644 index 0000000..a39444e --- /dev/null +++ b/drizzle/sqlite/meta/0010_snapshot.json @@ -0,0 +1,8284 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "251d9ba4-91ba-4d6f-aede-22f81c62de80", + "prevId": "54b875dd-78fc-4ae4-a00a-a6594bb31714", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_firings": { + "name": "alert_firings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fired_at": { + "name": "fired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'warning'" + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_alert_firings_rule": { + "name": "idx_alert_firings_rule", + "columns": [ + "rule_id", + "fired_at" + ], + "isUnique": false + }, + "idx_alert_firings_host": { + "name": "idx_alert_firings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "alert_firings_user_id_users_id_fk": { + "name": "alert_firings_user_id_users_id_fk", + "tableFrom": "alert_firings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_firings_rule_id_alert_rules_id_fk": { + "name": "alert_firings_rule_id_alert_rules_id_fk", + "tableFrom": "alert_firings", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rule_channels": { + "name": "alert_rule_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "rule_id": { + "name": "rule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rule_channels_rule_id_alert_rules_id_fk": { + "name": "alert_rule_channels_rule_id_alert_rules_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rule_channels_channel_id_notification_channels_id_fk": { + "name": "alert_rule_channels_channel_id_notification_channels_id_fk", + "tableFrom": "alert_rule_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "alert_rules": { + "name": "alert_rules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold_value": { + "name": "threshold_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold_duration_seconds": { + "name": "threshold_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "alert_rules_user_id_users_id_fk": { + "name": "alert_rules_user_id_users_id_fk", + "tableFrom": "alert_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_rules_host_id_ssh_data_id_fk": { + "name": "alert_rules_host_id_ssh_data_id_fk", + "tableFrom": "alert_rules", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "c2s_tunnel_presets": { + "name": "c2s_tunnel_presets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "computer_name": { + "name": "computer_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "c2s_tunnel_presets_user_id_users_id_fk": { + "name": "c2s_tunnel_presets_user_id_users_id_fk", + "tableFrom": "c2s_tunnel_presets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "command_history": { + "name": "command_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_command_history_user_host": { + "name": "idx_command_history_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "command_history_user_id_users_id_fk": { + "name": "command_history_user_id_users_id_fk", + "tableFrom": "command_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "command_history_host_id_ssh_data_id_fk": { + "name": "command_history_host_id_ssh_data_id_fk", + "tableFrom": "command_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_pinned": { + "name": "file_manager_pinned", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_pinned_user": { + "name": "idx_file_manager_pinned_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_pinned_user_id_users_id_fk": { + "name": "file_manager_pinned_user_id_users_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_pinned_host_id_ssh_data_id_fk": { + "name": "file_manager_pinned_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_pinned", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_recent": { + "name": "file_manager_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened": { + "name": "last_opened", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_recent_user": { + "name": "idx_file_manager_recent_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_recent_user_id_users_id_fk": { + "name": "file_manager_recent_user_id_users_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_recent_host_id_ssh_data_id_fk": { + "name": "file_manager_recent_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_manager_shortcuts": { + "name": "file_manager_shortcuts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_file_manager_shortcuts_user": { + "name": "idx_file_manager_shortcuts_user", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_manager_shortcuts_user_id_users_id_fk": { + "name": "file_manager_shortcuts_user_id_users_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_manager_shortcuts_host_id_ssh_data_id_fk": { + "name": "file_manager_shortcuts_host_id_ssh_data_id_fk", + "tableFrom": "file_manager_shortcuts", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_inventory": { + "name": "fleet_inventory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os_pretty_name": { + "name": "os_pretty_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kernel": { + "name": "kernel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "architecture": { + "name": "architecture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uptime_seconds": { + "name": "uptime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "package_manager": { + "name": "package_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_inventory_host": { + "name": "idx_fleet_inventory_host", + "columns": [ + "host_id", + "user_id" + ], + "isUnique": true + }, + "idx_fleet_inventory_user": { + "name": "idx_fleet_inventory_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_inventory_host_id_ssh_data_id_fk": { + "name": "fleet_inventory_host_id_ssh_data_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_inventory_user_id_users_id_fk": { + "name": "fleet_inventory_user_id_users_id_fk", + "tableFrom": "fleet_inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleet_members": { + "name": "fleet_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "fleet_id": { + "name": "fleet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_at": { + "name": "added_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_fleet_members_fleet_host": { + "name": "idx_fleet_members_fleet_host", + "columns": [ + "fleet_id", + "host_id" + ], + "isUnique": true + }, + "idx_fleet_members_host": { + "name": "idx_fleet_members_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fleet_members_fleet_id_fleets_id_fk": { + "name": "fleet_members_fleet_id_fleets_id_fk", + "tableFrom": "fleet_members", + "tableTo": "fleets", + "columnsFrom": [ + "fleet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fleet_members_host_id_ssh_data_id_fk": { + "name": "fleet_members_host_id_ssh_data_id_fk", + "tableFrom": "fleet_members", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fleets": { + "name": "fleets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tag_rules": { + "name": "tag_rules", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "fleets_sync_id_unique": { + "name": "fleets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fleets_user_id_users_id_fk": { + "name": "fleets_user_id_users_id_fk", + "tableFrom": "fleets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox": { + "name": "enable_proxmox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_config": { + "name": "proxmox_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_proxmox_stats": { + "name": "enable_proxmox_stats", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "proxmox_stats_config": { + "name": "proxmox_stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_topology": { + "name": "network_topology", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topology": { + "name": "topology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "network_topology_user_id_users_id_fk": { + "name": "network_topology_user_id_users_id_fk", + "tableFrom": "network_topology", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_node_history": { + "name": "proxmox_node_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxmox_node_history_host_id_ssh_data_id_fk": { + "name": "proxmox_node_history_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_node_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxmox_stats_preferences": { + "name": "proxmox_stats_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_proxmox_stats_prefs_user_host": { + "name": "idx_proxmox_stats_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "proxmox_stats_preferences_user_id_users_id_fk": { + "name": "proxmox_stats_preferences_user_id_users_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proxmox_stats_preferences_host_id_ssh_data_id_fk": { + "name": "proxmox_stats_preferences_host_id_ssh_data_id_fk", + "tableFrom": "proxmox_stats_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + }, + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_access": { + "name": "snippet_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "snippet_id": { + "name": "snippet_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'view'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_snippet_access_user_id": { + "name": "idx_snippet_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_snippet_access_snippet_id": { + "name": "idx_snippet_access_snippet_id", + "columns": [ + "snippet_id" + ], + "isUnique": false + }, + "idx_snippet_access_role_id": { + "name": "idx_snippet_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippet_access_snippet_id_snippets_id_fk": { + "name": "snippet_access_snippet_id_snippets_id_fk", + "tableFrom": "snippet_access", + "tableTo": "snippets", + "columnsFrom": [ + "snippet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_user_id_users_id_fk": { + "name": "snippet_access_user_id_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_role_id_roles_id_fk": { + "name": "snippet_access_role_id_roles_id_fk", + "tableFrom": "snippet_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "snippet_access_granted_by_users_id_fk": { + "name": "snippet_access_granted_by_users_id_fk", + "tableFrom": "snippet_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippet_folders": { + "name": "snippet_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "snippet_folders_sync_id_unique": { + "name": "snippet_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "snippet_folders_user_id_users_id_fk": { + "name": "snippet_folders_user_id_users_id_fk", + "tableFrom": "snippet_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snippets": { + "name": "snippets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "host_filter": { + "name": "host_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_note": { + "name": "is_note", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "snippets_sync_id_unique": { + "name": "snippets_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_snippets_user_id": { + "name": "idx_snippets_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "snippets_user_id_users_id_fk": { + "name": "snippets_user_id_users_id_fk", + "tableFrom": "snippets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "transfer_recent": { + "name": "transfer_recent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_host_id": { + "name": "source_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_host_id": { + "name": "dest_host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path": { + "name": "dest_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dest_path_label": { + "name": "dest_path_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_transfer_recent_user": { + "name": "idx_transfer_recent_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "transfer_recent_user_id_users_id_fk": { + "name": "transfer_recent_user_id_users_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_source_host_id_ssh_data_id_fk": { + "name": "transfer_recent_source_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "source_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transfer_recent_dest_host_id_ssh_data_id_fk": { + "name": "transfer_recent_dest_host_id_ssh_data_id_fk", + "tableFrom": "transfer_recent", + "tableTo": "ssh_data", + "columnsFrom": [ + "dest_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_workspaces": { + "name": "user_workspaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_workspaces_sync_id_unique": { + "name": "user_workspaces_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_user_workspaces_user_id": { + "name": "idx_user_workspaces_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_workspaces_user_id_users_id_fk": { + "name": "user_workspaces_user_id_users_id_fk", + "tableFrom": "user_workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json new file mode 100644 index 0000000..5839e4e --- /dev/null +++ b/drizzle/sqlite/meta/_journal.json @@ -0,0 +1,83 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1786147354086, + "tag": "0000_stormy_veda", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1786423594573, + "tag": "0001_colorful_the_call", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1786428073489, + "tag": "0002_woozy_turbo", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1786482020500, + "tag": "0003_premium_ultimates", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1786493047099, + "tag": "0004_cool_zuras", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1786498679199, + "tag": "0005_thin_sentry", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1786515116491, + "tag": "0006_nebulous_demogoblin", + "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1786519423219, + "tag": "0007_complex_nebula", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1786598521514, + "tag": "0008_fast_imperial_guard", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1786737720988, + "tag": "0009_pink_susan_delgado", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1786757019248, + "tag": "0010_mean_queen_noir", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/electron-builder.json b/electron-builder.json index 1386153..9ee5beb 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -122,8 +122,8 @@ "type": "distribution", "minimumSystemVersion": "10.15", "mergeASARs": false, - "singleArchFiles": "**/*.{node,bare}", - "x64ArchFiles": "**/*.{node,bare}" + "singleArchFiles": "**/*.{node,bare,dylib}", + "x64ArchFiles": "**/{*.{node,bare,dylib},node-pty/prebuilds/*/spawn-helper}" }, "dmg": { "artifactName": "termix_macos_${arch}_dmg.${ext}", diff --git a/electron/app-quit.cjs b/electron/app-quit.cjs new file mode 100644 index 0000000..18933b1 --- /dev/null +++ b/electron/app-quit.cjs @@ -0,0 +1,6 @@ +function quitApp(app, window) { + window?.destroy(); + app.quit(); +} + +module.exports = { quitApp }; diff --git a/electron/backend-paths.cjs b/electron/backend-paths.cjs new file mode 100644 index 0000000..b99791a --- /dev/null +++ b/electron/backend-paths.cjs @@ -0,0 +1,8 @@ +function getUnpackedAppRoot(appRoot) { + return appRoot.replace( + /app(-[a-z0-9]+)?\.asar(?!\.unpacked)/, + "app$1.asar.unpacked", + ); +} + +module.exports = { getUnpackedAppRoot }; diff --git a/electron/keyboard-shortcuts.cjs b/electron/keyboard-shortcuts.cjs new file mode 100644 index 0000000..5c3a516 --- /dev/null +++ b/electron/keyboard-shortcuts.cjs @@ -0,0 +1,12 @@ +function isCloseActiveTabInput(input) { + return ( + input.type === "keyDown" && + input.control === true && + input.alt !== true && + input.shift !== true && + input.meta !== true && + input.key.toLowerCase() === "w" + ); +} + +module.exports = { isCloseActiveTabInput }; diff --git a/electron/linux-password-store.cjs b/electron/linux-password-store.cjs new file mode 100644 index 0000000..eb8cfbf --- /dev/null +++ b/electron/linux-password-store.cjs @@ -0,0 +1,42 @@ +// Chromium derives safeStorage's backend from the running desktop and falls back +// to the "basic_text" store for anything it has no mapping for, which is every +// wlroots-style compositor (Hyprland, sway, niri, river, ...). +// safeStorage.isEncryptionAvailable() reports false for that store, so every +// credential the desktop app persists through it -- the remote sync JWT, the +// Electron auth cookie -- is refused at the point of writing. The refusal is +// invisible from the outside: the user signs in, nothing is stored, and the next +// sync tick reports the session as expired rather than as never saved. +// +// Those desktops still run an ordinary Secret Service (gnome-keyring, KWallet's +// compatibility service, KeePassXC, ...), so naming the libsecret backend is +// enough to make encryption available again. Desktops whose auto-detection +// already resolves to KWallet keep it, and an explicit --password-store from the +// user always wins. +// +// Moving a machine off "basic_text" cannot orphan stored secrets: nothing was +// ever written there, because isEncryptionAvailable() gated every write. +const KWALLET_DESKTOPS = /\b(kde|plasma|lxqt)\b/i; +const LIBSECRET_STORE = "gnome-libsecret"; + +/** + * Picks safeStorage's backend on Linux, and returns the store it selected (or + * null when auto-detection was left to decide). + * + * Must run before the app is ready: Chromium reads the switch when the store is + * first opened, and appending it afterwards has no effect. + */ +function selectLinuxPasswordStore(commandLine, env) { + if (commandLine.hasSwitch("password-store")) { + return null; + } + + const desktop = `${env.XDG_CURRENT_DESKTOP || ""}:${env.DESKTOP_SESSION || ""}`; + if (KWALLET_DESKTOPS.test(desktop)) { + return null; + } + + commandLine.appendSwitch("password-store", LIBSECRET_STORE); + return LIBSECRET_STORE; +} + +module.exports = { selectLinuxPasswordStore }; diff --git a/electron/main.cjs b/electron/main.cjs index 1396320..651723b 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -12,14 +12,78 @@ const { nativeImage, } = require("electron"); const path = require("path"); +const { getUnpackedAppRoot } = require("./backend-paths.cjs"); const fs = require("fs"); const os = require("os"); const https = require("https"); const http = require("http"); const net = require("net"); +const tls = require("tls"); +const zlib = require("zlib"); +const crypto = require("crypto"); const { URL } = require("url"); const { fork, spawn } = require("child_process"); +const pty = require("node-pty"); const WebSocket = require("ws"); +const remoteSync = require("./remote-sync.cjs"); +const { launchNativeRdp } = require("./native-rdp.cjs"); +const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs"); +const { quitApp } = require("./app-quit.cjs"); +const { selectLinuxPasswordStore } = require("./linux-password-store.cjs"); + +const localTerminalSessions = new Map(); + +function localShell() { + if (process.platform === "win32") { + return { + file: process.env.TERMIX_LOCAL_SHELL || "powershell.exe", + args: ["-NoLogo"], + }; + } + return { + file: + process.env.TERMIX_LOCAL_SHELL || + process.env.SHELL || + (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"), + args: ["-l"], + }; +} + +function ownedLocalTerminal(event, sessionId) { + if (typeof sessionId !== "string" || !/^[a-f0-9-]{36}$/.test(sessionId)) { + return null; + } + const session = localTerminalSessions.get(sessionId); + return session?.ownerId === event.sender.id ? session : null; +} + +function closeLocalTerminalsFor(ownerId) { + for (const [sessionId, session] of localTerminalSessions) { + if (session.ownerId !== ownerId) continue; + session.process.kill(); + localTerminalSessions.delete(sessionId); + } +} + +// The main process's Node.js networking (the `https`/`http` modules used by +// httpFetch below, and the global `fetch` used by remote-sync.cjs) only +// trusts Node's bundled Mozilla CA list by default, not the OS/system trust +// store. Chromium (the renderer, i.e. the web app and the login iframe) uses +// the OS trust store instead, so a certificate that's valid in-browser -- +// e.g. one issued by a reverse proxy's internal/corporate CA, or a system +// CA installed via Keychain/certmgr -- can still fail main-process requests +// with UNABLE_TO_VERIFY_LEAF_SIGNATURE. Merge the system store in so remote +// sync and the connection health check see the same trust as the browser. +try { + if (typeof tls.setDefaultCACertificates === "function") { + tls.setDefaultCACertificates([ + ...tls.getCACertificates("default"), + ...tls.getCACertificates("system"), + ]); + } +} catch (error) { + console.error("Failed to merge system CA certificates:", error); +} // Portable mode: if a `.portable` marker exists next to the executable, // store all data in a `data` folder beside the exe instead of %APPDATA%. @@ -441,7 +505,10 @@ function isInvalidCertificateAllowedForUrl(url) { // fall through } - const config = getServerConfigSync(); + // The only remaining "connected remote server" a self-signed/invalid + // certificate could legitimately apply to is the Remote Sync server + // (also used for C2S tunnel relaying, see getC2SRelayUrl). + const config = remoteSync.getRemoteSyncConfig(); if (!config?.allowInvalidCertificate || !config?.serverUrl) return false; return getOrigin(url) === getOrigin(config.serverUrl); @@ -474,9 +541,31 @@ function httpFetch(url, options = {}) { }; const req = client.request(url, requestOptions, (res) => { - let data = ""; - res.on("data", (chunk) => (data += chunk)); - res.on("end", () => { + const chunks = []; + // Reverse proxies (nginx and friends) commonly gzip/deflate/br-compress + // responses regardless of client Accept-Encoding. Unlike browser fetch, + // Node's http/https modules never auto-decompress, so an unhandled + // content-encoding here silently turns the body into garbage bytes. + let stream = res; + const encoding = (res.headers["content-encoding"] || "") + .toLowerCase() + .trim(); + try { + if (encoding === "gzip" || encoding === "x-gzip") { + stream = res.pipe(zlib.createGunzip()); + } else if (encoding === "br") { + stream = res.pipe(zlib.createBrotliDecompress()); + } else if (encoding === "deflate") { + stream = res.pipe(zlib.createInflate()); + } + } catch (decompressError) { + reject(decompressError); + return; + } + + stream.on("data", (chunk) => chunks.push(chunk)); + stream.on("end", () => { + const data = Buffer.concat(chunks).toString("utf8"); resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, @@ -484,6 +573,7 @@ function httpFetch(url, options = {}) { json: () => Promise.resolve(JSON.parse(data)), }); }); + stream.on("error", reject); }); req.on("error", reject); @@ -508,6 +598,13 @@ if (process.platform === "linux") { // Chromium's hit-testing uses unscaled coords while the compositor scales visually, // so forcing scale factor 1 keeps them in sync. See: https://github.com/brave/brave-browser/issues/50028 app.commandLine.appendSwitch("--force-device-scale-factor", "1"); + + const passwordStore = selectLinuxPasswordStore(app.commandLine, process.env); + if (passwordStore) { + logToFile( + `[safeStorage] Selected the ${passwordStore} password store for this desktop.`, + ); + } } if (process.platform === "win32") { @@ -796,10 +893,7 @@ function getBackendPaths() { // fork() does not go through Electron's asar redirector โ€” use the unpacked path. // On macOS multi-arch builds (mergeASARs: false), electron-builder names the ASAR // app-arm64.asar / app-x64.asar instead of app.asar, so match all variants. - const unpackedRoot = appRoot.replace( - /app(-[a-z0-9]+)?\.asar(?!\.unpacked)/, - "app.asar.unpacked", - ); + const unpackedRoot = getUnpackedAppRoot(appRoot); const backendDir = path.join(unpackedRoot, "dist", "backend", "backend"); return { entryPath: path.join(backendDir, "starter.js"), @@ -816,7 +910,62 @@ function getBackendDataDir() { return dataDir; } +function getBackendPidFilePath() { + return path.join(app.getPath("userData"), "backend.pid"); +} + +// If the app was previously killed abnormally (crash, force-quit, Task +// Manager) rather than through the normal quit flow, will-quit never fires +// and stopBackendServer() never runs -- the forked backend child is a +// genuinely separate OS process on Windows/mac/Linux, so it keeps running +// and holding every port the backend binds (30001, 30003-30008, 30010, +// 30012...). Every subsequent launch's own backend then fails outright +// with EADDRINUSE and the app is stuck until something manually kills the +// orphan. Reap any such leftover process, identified by PID file, before +// spawning a new one. +function reapOrphanedBackendProcess() { + const pidFilePath = getBackendPidFilePath(); + let recordedPid; + try { + recordedPid = parseInt(fs.readFileSync(pidFilePath, "utf8").trim(), 10); + } catch { + return; + } + if (!Number.isInteger(recordedPid) || recordedPid <= 0) return; + + try { + // Signal 0 does not kill the process -- it only checks whether a + // process with this PID exists and is signalable, throwing ESRCH if + // not. This avoids killing an unrelated process that happens to have + // reused the same PID since the last run. + process.kill(recordedPid, 0); + } catch { + // No live process at that PID; nothing to reap. + try { + fs.unlinkSync(pidFilePath); + } catch { + // already absent + } + return; + } + + logToFile( + `Found orphaned backend process from a previous session (pid ${recordedPid}), terminating it before starting a new one`, + ); + try { + process.kill(recordedPid, "SIGKILL"); + } catch { + // already gone + } + try { + fs.unlinkSync(pidFilePath); + } catch { + // already absent + } +} + function startBackendServer() { + reapOrphanedBackendProcess(); return new Promise((resolve) => { const { entryPath, backendCwd } = getBackendPaths(); @@ -852,11 +1001,17 @@ function startBackendServer() { NODE_ENV: "production", ELECTRON_EMBEDDED: "true", PORT: "30001", + VERSION: app.getVersion(), }, stdio: ["pipe", "pipe", "pipe", "ipc"], }); logToFile("Backend process spawned, pid:", backendProcess.pid); + try { + fs.writeFileSync(getBackendPidFilePath(), String(backendProcess.pid)); + } catch { + // Non-fatal: only means a future crash won't self-heal via reap. + } let resolved = false; const readyTimeout = setTimeout(() => { @@ -888,6 +1043,7 @@ function startBackendServer() { backendStartFailed = true; } backendProcess = null; + clearBackendPidFile(); if (!resolved) { resolved = true; clearTimeout(readyTimeout); @@ -907,6 +1063,14 @@ function startBackendServer() { }); } +function clearBackendPidFile() { + try { + fs.unlinkSync(getBackendPidFilePath()); + } catch { + // already absent + } +} + function stopBackendServer() { if (!backendProcess) return; @@ -929,6 +1093,7 @@ function stopBackendServer() { backendProcess.on("exit", () => { clearTimeout(forceKillTimeout); backendProcess = null; + clearBackendPidFile(); }); } @@ -991,7 +1156,7 @@ function createTray() { label: "Quit", click: () => { isQuitting = true; - app.quit(); + quitApp(app, mainWindow); }, }, ]); @@ -1070,6 +1235,13 @@ function createWindow() { const customUserAgent = `Termix-Desktop/${appVersion} (${platform}; Electron/${electronVersion})`; mainWindow.webContents.setUserAgent(customUserAgent); + mainWindow.webContents.on("before-input-event", (event, input) => { + if (process.platform !== "win32" || !isCloseActiveTabInput(input)) return; + + event.preventDefault(); + mainWindow.webContents.send("close-active-tab"); + }); + mainWindow.webContents.session.webRequest.onBeforeSendHeaders( (details, callback) => { details.requestHeaders["X-Electron-App"] = "true"; @@ -1331,11 +1503,14 @@ ipcMain.handle("get-platform", () => { return process.platform; }); +ipcMain.handle("open-native-rdp", (_event, options) => + launchNativeRdp(options), +); + ipcMain.handle("get-embedded-server-status", () => { return { running: backendProcess !== null && !backendProcess.killed && !backendStartFailed, - embedded: !isDev, dataDir: isDev ? null : getBackendDataDir(), }; }); @@ -1442,6 +1617,84 @@ ipcMain.handle("save-server-config", (event, config) => { } }); +// --- Remote sync (optional desktop <-> self-hosted server sync) --- + +// Surfaces the pre-standalone-rework server-config.json (if a serverUrl was +// ever set in it) so the renderer can prompt upgraded installs to set up +// Remote Sync -- their hosts live on that old server and won't appear +// locally until sync is enabled. A fresh install never had this file, so +// this is naturally false for anyone who never used the old architecture. +ipcMain.handle("get-legacy-server-config", () => { + const config = getServerConfigSync(); + return { serverUrl: config?.serverUrl || null }; +}); + +ipcMain.handle("get-desktop-settings", () => { + return remoteSync.getDesktopSettings(); +}); + +ipcMain.handle("save-desktop-settings", (_event, settings) => { + return remoteSync.saveDesktopSettings(settings); +}); + +ipcMain.handle("get-remote-sync-config", () => { + return remoteSync.getRemoteSyncConfig(); +}); + +ipcMain.handle("save-remote-sync-config", (_event, config) => { + return remoteSync.saveRemoteSyncConfig(config); +}); + +ipcMain.handle("clear-remote-sync-config", async () => { + const result = remoteSync.clearRemoteSyncConfig(); + remoteSync.clearRemoteSyncJwt(); + remoteSync.getRemoteSyncEngine()?.updateStatus({ + connected: false, + syncing: false, + needsReauth: false, + lastError: null, + }); + return result; +}); + +ipcMain.handle("save-remote-sync-jwt", (_event, token) => { + const result = remoteSync.saveRemoteSyncJwt(token); + if (result.success) { + remoteSync.getRemoteSyncEngine()?.updateStatus({ + connected: true, + needsReauth: false, + lastError: null, + }); + remoteSync.getRemoteSyncEngine()?.syncNow(); + } + return result; +}); + +ipcMain.handle("get-remote-sync-jwt", () => { + return remoteSync.getRemoteSyncJwt(); +}); + +ipcMain.handle("clear-remote-sync-jwt", () => { + return remoteSync.clearRemoteSyncJwt(); +}); + +ipcMain.handle("get-remote-sync-status", () => { + return remoteSync.getRemoteSyncEngine()?.status || null; +}); + +ipcMain.handle("get-remote-sync-user-info", () => { + return remoteSync.getRemoteSyncUserInfo(); +}); + +ipcMain.handle("remote-sync-now", async () => { + return (await remoteSync.getRemoteSyncEngine()?.syncNow()) || null; +}); + +ipcMain.handle("notify-local-login", (_event, token) => { + remoteSync.getRemoteSyncEngine()?.setLocalJwt(token); + return { success: true }; +}); + function getC2STunnelConfigPath() { return path.join(app.getPath("userData"), "c2s-tunnels.json"); } @@ -1577,36 +1830,33 @@ const C2S_WS_HIGH_WATERMARK = 1024 * 1024; const C2S_WS_LOW_WATERMARK = 256 * 1024; const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024; +// C2S (client-to-server) tunnels relay through a connected, self-hosted +// Termix server -- the same "remote server" concept Remote Sync connects +// to, not the always-local embedded backend. There's no separate C2S +// server-URL setting in the UI; it has always shared whatever remote +// server the rest of the app was pointed at. Before the standalone-first +// rework that was server-config.json; now it's remote-sync-config.json, +// since that's the only remaining notion of "a connected remote server." function getC2SRelayUrl() { - const config = getServerConfigSync(); - const serverUrl = - config?.serverUrl || (!isDev ? "http://127.0.0.1:30003" : null); + const config = remoteSync.getRemoteSyncConfig(); + const serverUrl = config?.serverUrl; if (!serverUrl) { - throw new Error("No Termix server configured"); + throw new Error( + "No remote Termix server connected -- enable Remote Sync first", + ); } const base = serverUrl.replace(/\/$/, ""); - const relayHttpUrl = base.endsWith(":30003") - ? `${base}/ssh/tunnel/c2s/stream` - : `${base}/ssh/tunnel/c2s/stream`; + const relayHttpUrl = `${base}/ssh/tunnel/c2s/stream`; return relayHttpUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:"); } -async function getC2SRelayHeaders(relayUrl) { - if (!mainWindow?.webContents?.session) return {}; - - const cookieUrl = relayUrl - .replace(/^ws:/, "http:") - .replace(/^wss:/, "https:"); - const cookies = await mainWindow.webContents.session.cookies.get({ - url: cookieUrl, - name: "jwt", - }); - const jwt = cookies[0]?.value; +async function getC2SRelayHeaders() { + const jwt = remoteSync.getRemoteSyncJwt(); if (!jwt) return {}; return { - Cookie: `jwt=${encodeURIComponent(jwt)}`, + Authorization: `Bearer ${jwt}`, }; } @@ -1706,7 +1956,7 @@ async function openC2SRelay( ) { const tunnelName = tunnel.name || getC2STunnelName(tunnel); const relayUrl = getC2SRelayUrl(); - const headers = await getC2SRelayHeaders(relayUrl); + const headers = await getC2SRelayHeaders(); logToFile(`[c2s] opening relay for ${tunnelName}`, { relayUrl, targetHost, @@ -1811,7 +2061,7 @@ async function openC2SRelay( async function testC2SRelay(tunnel, targetHost, targetPort) { const relayUrl = getC2SRelayUrl(); - const headers = await getC2SRelayHeaders(relayUrl); + const headers = await getC2SRelayHeaders(); const ws = new WebSocket( relayUrl, getWebSocketOptions(relayUrl, { headers }), @@ -2090,7 +2340,7 @@ async function startC2SRemoteTunnel(tunnel, index = 0) { } const relayUrl = getC2SRelayUrl(); - const headers = await getC2SRelayHeaders(relayUrl); + const headers = await getC2SRelayHeaders(); const ws = new WebSocket( relayUrl, getWebSocketOptions(relayUrl, { headers }), @@ -2346,9 +2596,39 @@ async function startC2STunnel(tunnel, index = 0) { `[c2s] listening for ${tunnelName} on ${bindHost}:${sourcePort}`, ); setC2STunnelStatus(tunnelName, { - connected: true, - status: "CONNECTED", + connected: false, + status: "CONNECTING", + reason: "Verifying endpoint SSH connection", }); + + const verifyTunnel = + mode === "dynamic" + ? testC2SRelay( + { ...tunnel, name: `${tunnelName}::verify`, mode }, + undefined, + undefined, + ) + : testC2SRelay( + { ...tunnel, name: `${tunnelName}::verify`, mode }, + tunnel.targetHost || "127.0.0.1", + Number(tunnel.endpointPort), + ); + + verifyTunnel.then((result) => { + if (!c2sTunnelRuntimes.has(tunnelName)) return; + if (result.success) { + setC2STunnelStatus(tunnelName, { + connected: true, + status: "CONNECTED", + }); + } else { + setC2STunnelError( + tunnelName, + result.error || "Endpoint SSH connection failed", + ); + } + }); + resolve({ success: true, tunnelName }); }); }); @@ -2644,6 +2924,91 @@ ipcMain.handle("clipboard-write-text", (_event, text) => { ipcMain.handle("clipboard-read-text", () => clipboard.readText()); +ipcMain.handle("local-terminal-start", (event, dimensions = {}) => { + const cols = Math.min(500, Math.max(2, Number(dimensions.cols) || 80)); + const rows = Math.min(300, Math.max(1, Number(dimensions.rows) || 24)); + const sessionId = crypto.randomUUID(); + const shellConfig = localShell(); + const child = pty.spawn(shellConfig.file, shellConfig.args, { + name: "xterm-256color", + cols, + rows, + cwd: os.homedir(), + env: { + ...process.env, + TERM: "xterm-256color", + COLORTERM: "truecolor", + }, + }); + const ownerId = event.sender.id; + const session = { ownerId, process: child, ready: false, buffered: "" }; + localTerminalSessions.set(sessionId, session); + child.onData((data) => { + if (!session.ready) { + session.buffered = (session.buffered + data).slice(-1024 * 1024); + return; + } + if (!event.sender.isDestroyed()) { + event.sender.send(`local-terminal:data:${sessionId}`, data); + } + }); + child.onExit(({ exitCode }) => { + localTerminalSessions.delete(sessionId); + if (!event.sender.isDestroyed()) { + event.sender.send(`local-terminal:exit:${sessionId}`, exitCode); + } + }); + event.sender.once("destroyed", () => closeLocalTerminalsFor(ownerId)); + return { sessionId, shell: shellConfig.file }; +}); + +ipcMain.handle("local-terminal-ready", (event, sessionId) => { + const session = ownedLocalTerminal(event, sessionId); + if (!session) return false; + session.ready = true; + if (session.buffered && !event.sender.isDestroyed()) { + event.sender.send(`local-terminal:data:${sessionId}`, session.buffered); + session.buffered = ""; + } + return true; +}); + +ipcMain.handle("local-terminal-write", (event, sessionId, data) => { + const session = ownedLocalTerminal(event, sessionId); + if (!session || typeof data !== "string" || data.length > 64 * 1024) { + return false; + } + session.process.write(data); + return true; +}); + +ipcMain.handle("local-terminal-resize", (event, sessionId, cols, rows) => { + const session = ownedLocalTerminal(event, sessionId); + const width = Number(cols); + const height = Number(rows); + if ( + !session || + !Number.isInteger(width) || + !Number.isInteger(height) || + width < 2 || + width > 500 || + height < 1 || + height > 300 + ) { + return false; + } + session.process.resize(width, height); + return true; +}); + +ipcMain.handle("local-terminal-close", (event, sessionId) => { + const session = ownedLocalTerminal(event, sessionId); + if (!session) return false; + localTerminalSessions.delete(sessionId); + session.process.kill(); + return true; +}); + ipcMain.handle("show-save-dialog", async (_event, options) => { return dialog.showSaveDialog(mainWindow, options || {}); }); @@ -2772,31 +3137,33 @@ ipcMain.handle("close-external-editor", (_event, editId) => { ipcMain.handle("test-server-connection", async (event, serverUrl) => { try { const normalizedServerUrl = serverUrl.replace(/\/$/, ""); - const healthUrl = `${normalizedServerUrl}/health`; + // This is a best-effort reachability probe, not a hard gate: a reverse + // proxy doing SSO in front of the real server (Pangolin, Authelia, + // Cloudflare Access, etc.) intercepts this unauthenticated request + // before it ever reaches Termix's own /health route, and returns its + // own login page (HTML, or a redirect) instead of {"status":"ok"}. + // That's a legitimate, working setup -- the login iframe shown right + // after this check is what actually proves the server is real, by + // completing an authenticated round-trip. So any response at all here + // (any status code, any body) means "something is there, let the user + // proceed"; only a network-level failure (nothing answered at all) + // blocks continuing. try { const response = await httpFetch(healthUrl, { method: "GET", timeout: 10000, }); - if (response.ok) { - const data = await response.text(); - - if ( - data.includes("") || - data.includes("") - ) { - return { - success: false, - error: - "Server returned HTML instead of JSON. This does not appear to be a Termix server.", - }; - } + const data = await response.text(); + const looksLikeHtml = + data.includes("") || + data.includes(""); + if (response.ok && !looksLikeHtml) { try { const healthData = JSON.parse(data); if ( @@ -2816,64 +3183,27 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => { console.log("Health endpoint did not return valid JSON"); } } + + // Reachable, but not a recognized Termix health response -- likely a + // proxy/SSO login page in front of the real server. Let the user + // proceed; the login step next will fail clearly if this really + // isn't a Termix server. + return { + success: true, + status: response.status, + testedUrl: healthUrl, + warning: looksLikeHtml + ? "Could not confirm this is a Termix server (the response looked like an HTML page, which can happen behind a login-protected reverse proxy). You can continue, and the next step will fail clearly if this isn't actually a Termix server." + : "Server responded, but not with the expected health check format. Continuing anyway.", + }; } catch (urlError) { console.error("Health check failed:", urlError); + return { + success: false, + error: + "Server is not responding. Please ensure the server is running and accessible.", + }; } - - try { - const versionUrl = `${normalizedServerUrl}/version`; - const response = await httpFetch(versionUrl, { - method: "GET", - timeout: 10000, - }); - - if (response.ok) { - const data = await response.text(); - - if ( - data.includes("") || - data.includes("") - ) { - return { - success: false, - error: - "Server returned HTML instead of JSON. This does not appear to be a Termix server.", - }; - } - - try { - const versionData = JSON.parse(data); - if ( - versionData && - (versionData.status === "up_to_date" || - versionData.status === "requires_update" || - (versionData.localVersion && - versionData.version && - versionData.latest_release)) - ) { - return { - success: true, - status: response.status, - testedUrl: versionUrl, - warning: - "Health endpoint not available, but server appears to be running", - }; - } - } catch (parseError) { - console.log("Version endpoint did not return valid JSON"); - } - } - } catch (versionError) { - console.error("Version check failed:", versionError); - } - - return { - success: false, - error: - "Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.", - }; } catch (error) { return { success: false, error: error.message }; } @@ -2967,6 +3297,7 @@ app.whenReady().then(async () => { createTray(); createWindow(); + remoteSync.initRemoteSync(() => mainWindow); logToFile("=== Startup complete ==="); }); diff --git a/electron/native-rdp.cjs b/electron/native-rdp.cjs new file mode 100644 index 0000000..49dd688 --- /dev/null +++ b/electron/native-rdp.cjs @@ -0,0 +1,85 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawn } = require("child_process"); + +function singleLine(value, maxLength = 255) { + return String(value ?? "") + .replace(/[\r\n\0]/g, "") + .trim() + .slice(0, maxLength); +} + +function validateNativeRdpOptions(options) { + const host = singleLine(options?.host); + const port = Number(options?.port ?? 3389); + if (!host || host.length > 253 || /[\\/]/.test(host)) { + throw new Error("Invalid RDP host"); + } + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error("Invalid RDP port"); + } + return { + host, + port, + username: singleLine(options?.username), + domain: singleLine(options?.domain), + }; +} + +function buildRdpFile(options) { + const { host, port, username, domain } = validateNativeRdpOptions(options); + const address = host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`; + const qualifiedUsername = username + ? domain + ? `${domain}\\${username}` + : username + : ""; + return [ + `full address:s:${address}`, + "prompt for credentials:i:1", + "administrative session:i:0", + ...(qualifiedUsername ? [`username:s:${qualifiedUsername}`] : []), + "", + ].join("\r\n"); +} + +async function launchNativeRdp(options, platform = process.platform) { + if (platform !== "win32") { + return { + success: false, + error: "Windows Remote Desktop is only available on Windows", + }; + } + + const rdpContent = buildRdpFile(options); + const tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), "termix-rdp-"), + ); + const rdpPath = path.join(tempDir, "connection.rdp"); + await fs.promises.writeFile(rdpPath, rdpContent, { + encoding: "utf8", + mode: 0o600, + }); + + return new Promise((resolve) => { + const child = spawn("mstsc.exe", [rdpPath], { + detached: true, + windowsHide: true, + stdio: "ignore", + }); + const cleanup = () => + fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + child.once("error", (error) => { + cleanup(); + resolve({ success: false, error: error.message }); + }); + child.once("spawn", () => { + child.unref(); + setTimeout(cleanup, 30_000).unref(); + resolve({ success: true }); + }); + }); +} + +module.exports = { buildRdpFile, launchNativeRdp, validateNativeRdpOptions }; diff --git a/electron/preload.js b/electron/preload.js index 6b9cf1c..b045cf6 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -2,6 +2,8 @@ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { getAppVersion: () => ipcRenderer.invoke("get-app-version"), + getPlatform: () => ipcRenderer.invoke("get-platform"), + openNativeRdp: (options) => ipcRenderer.invoke("open-native-rdp", options), removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel), isElectron: true, @@ -31,6 +33,18 @@ contextBridge.exposeInMainWorld("electronAPI", { startC2SAutoStartTunnels: () => ipcRenderer.invoke("start-c2s-autostart-tunnels"), + onRemoteSyncStatusChanged: (callback) => { + const listener = (_event, status) => callback(status); + ipcRenderer.on("remote-sync-status-changed", listener); + return () => + ipcRenderer.removeListener("remote-sync-status-changed", listener); + }, + onCloseActiveTab: (callback) => { + const listener = () => callback(); + ipcRenderer.on("close-active-tab", listener); + return () => ipcRenderer.removeListener("close-active-tab", listener); + }, + clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"), getSessionCookie: (name, targetUrl) => ipcRenderer.invoke("get-session-cookie", name, targetUrl), @@ -66,6 +80,29 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.invoke("start-drag-to-desktop", dragData), cleanupTempFile: (tempId) => ipcRenderer.invoke("cleanup-temp-file", tempId), + startLocalTerminal: (dimensions) => + ipcRenderer.invoke("local-terminal-start", dimensions), + writeLocalTerminal: (sessionId, data) => + ipcRenderer.invoke("local-terminal-write", sessionId, data), + readyLocalTerminal: (sessionId) => + ipcRenderer.invoke("local-terminal-ready", sessionId), + resizeLocalTerminal: (sessionId, cols, rows) => + ipcRenderer.invoke("local-terminal-resize", sessionId, cols, rows), + closeLocalTerminal: (sessionId) => + ipcRenderer.invoke("local-terminal-close", sessionId), + onLocalTerminalData: (sessionId, callback) => { + const channel = `local-terminal:data:${sessionId}`; + const listener = (_event, data) => callback(data); + ipcRenderer.on(channel, listener); + return () => ipcRenderer.removeListener(channel, listener); + }, + onLocalTerminalExit: (sessionId, callback) => { + const channel = `local-terminal:exit:${sessionId}`; + const listener = (_event, exitCode) => callback(exitCode); + ipcRenderer.on(channel, listener); + return () => ipcRenderer.removeListener(channel, listener); + }, + invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), }); diff --git a/electron/remote-sync-entities.cjs b/electron/remote-sync-entities.cjs new file mode 100644 index 0000000..a60ce8c --- /dev/null +++ b/electron/remote-sync-entities.cjs @@ -0,0 +1,15 @@ +const SYNCED_ENTITY_TYPES = Object.freeze([ + // Ordered by reference dependency: hosts and snippets resolve credential, + // vault and folder syncIds, so those have to exist on the other side first. + "sshCredentials", + "vaultProfiles", + "sshFolders", + "snippetFolders", + "hosts", + "snippets", + "dashboardServiceLinks", + "homepageItems", + "userPreferences", +]); + +module.exports = { SYNCED_ENTITY_TYPES }; diff --git a/electron/remote-sync.cjs b/electron/remote-sync.cjs new file mode 100644 index 0000000..507c512 --- /dev/null +++ b/electron/remote-sync.cjs @@ -0,0 +1,581 @@ +// Remote sync engine for the desktop app's optional connection to a +// self-hosted Termix server. Runs entirely in the Electron main process: +// - Holds the remote JWT (safeStorage-encrypted on disk, never exposed to +// the renderer's localStorage) and the local embedded backend's JWT +// (cached in memory only, handed over by the renderer at local-login +// time via notify-local-login). +// - On a timer, pulls + pushes each synced entity type between the +// embedded backend (always localhost:30001) and the configured remote +// server, reconciling by syncId with last-write-wins on updatedAt, and +// propagating tombstones (deletions) in both directions. +// - Pushes connection/sync status to the renderer via IPC so the Settings +// UI and a global banner can reflect it without polling. + +const { app, safeStorage } = require("electron"); +const fs = require("fs"); +const path = require("path"); +const { SYNCED_ENTITY_TYPES } = require("./remote-sync-entities.cjs"); + +const SYNC_INTERVAL_MS = 90 * 1000; +const EMBEDDED_BASE_URL = "http://127.0.0.1:30001"; + +function dataPath(filename) { + return path.join(app.getPath("userData"), filename); +} + +function readJson(filePath, fallback) { + try { + if (!fs.existsSync(filePath)) return fallback; + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch { + return fallback; + } +} + +function writeJson(filePath, value) { + const userDataPath = app.getPath("userData"); + if (!fs.existsSync(userDataPath)) { + fs.mkdirSync(userDataPath, { recursive: true }); + } + fs.writeFileSync(filePath, JSON.stringify(value, null, 2)); +} + +function getDesktopSettingsPath() { + return dataPath("desktop-settings.json"); +} + +function getRemoteSyncConfigPath() { + return dataPath("remote-sync-config.json"); +} + +function getRemoteSyncCredentialPath() { + return dataPath("remote-sync-credential.json"); +} + +function getRemoteSyncStatePath() { + return dataPath("remote-sync-state.json"); +} + +function getDesktopSettings() { + return readJson(getDesktopSettingsPath(), { + defaultConnectionOrigin: "local", + migrationNoticeAcknowledged: false, + }); +} + +function saveDesktopSettings(settings) { + writeJson(getDesktopSettingsPath(), settings); + return { success: true }; +} + +function getRemoteSyncConfig() { + return readJson(getRemoteSyncConfigPath(), null); +} + +function saveRemoteSyncConfig(config) { + writeJson(getRemoteSyncConfigPath(), config); + return { success: true }; +} + +function clearRemoteSyncConfig() { + try { + fs.unlinkSync(getRemoteSyncConfigPath()); + } catch { + // already absent + } + return { success: true }; +} + +function getSafeStorageAvailable() { + try { + return safeStorage.isEncryptionAvailable(); + } catch { + return false; + } +} + +function saveRemoteSyncJwt(token) { + if (!getSafeStorageAvailable()) { + // Carries a stable reason alongside the message: the renderer has a + // translated explanation for this one, because "no OS keyring" is a + // machine-level problem the user has to go and fix, not something signing + // in again can resolve. + return { + success: false, + reason: "encryption_unavailable", + error: "Encryption unavailable on this system", + }; + } + writeJson(getRemoteSyncCredentialPath(), { + encrypted: true, + value: safeStorage.encryptString(token).toString("base64"), + obtainedAt: new Date().toISOString(), + }); + return { success: true }; +} + +function getRemoteSyncJwt() { + const record = readJson(getRemoteSyncCredentialPath(), null); + if (!record?.encrypted || !getSafeStorageAvailable()) return null; + try { + return safeStorage.decryptString(Buffer.from(record.value, "base64")); + } catch { + return null; + } +} + +function clearRemoteSyncJwt() { + try { + fs.unlinkSync(getRemoteSyncCredentialPath()); + } catch { + // already absent + } + return { success: true }; +} + +async function getRemoteSyncUserInfo() { + const config = getRemoteSyncConfig(); + const token = getRemoteSyncJwt(); + if (!config?.serverUrl || !token || isJwtExpiredOrExpiringSoon(token)) { + return null; + } + + const baseUrl = config.serverUrl.replace(/\/$/, ""); + const userResponse = await fetch(`${baseUrl}/users/me`, { + headers: { Authorization: `Bearer ${token}`, "X-Electron-App": "true" }, + }); + if (!userResponse.ok) return null; + + const user = await userResponse.json(); + const rolesResponse = await fetch( + `${baseUrl}/rbac/users/${encodeURIComponent(user.userId)}/roles`, + { + headers: { Authorization: `Bearer ${token}`, "X-Electron-App": "true" }, + }, + ); + const roles = rolesResponse.ok + ? (await rolesResponse.json()).roles || [] + : []; + + return { + userId: user.userId, + username: user.username, + is_admin: !!user.is_admin, + is_oidc: !!user.is_oidc, + is_dual_auth: !!user.is_dual_auth, + totp_enabled: !!user.totp_enabled, + roles, + }; +} + +function decodeJwtExpiry(token) { + try { + const payloadB64 = token.split(".")[1]; + const payload = JSON.parse( + Buffer.from(payloadB64, "base64").toString("utf8"), + ); + return typeof payload.exp === "number" ? payload.exp * 1000 : null; + } catch { + return null; + } +} + +function isJwtExpiredOrExpiringSoon(token, marginMs = 60 * 1000) { + const expiresAt = decodeJwtExpiry(token); + if (expiresAt === null) return false; + return Date.now() + marginMs >= expiresAt; +} + +class RemoteSyncEngine { + constructor(getMainWindow) { + this.getMainWindow = getMainWindow; + this.localJwt = null; + this.timer = null; + this.syncing = false; + this.status = { + connected: false, + syncing: false, + lastSyncedAt: null, + lastError: null, + needsReauth: false, + }; + } + + setLocalJwt(token) { + this.localJwt = token || null; + } + + emitStatus() { + const win = this.getMainWindow?.(); + if (!win || win.isDestroyed()) return; + win.webContents.send("remote-sync-status-changed", this.status); + } + + updateStatus(patch) { + this.status = { ...this.status, ...patch }; + this.emitStatus(); + } + + start() { + const config = getRemoteSyncConfig(); + this.status.connected = !!config?.serverUrl; + if (this.timer) clearInterval(this.timer); + this.timer = setInterval(() => this.syncNow(), SYNC_INTERVAL_MS); + if (config?.serverUrl) { + // Fire an initial sync shortly after startup rather than waiting a + // full interval, but don't block app boot on it. + setTimeout(() => this.syncNow(), 5000); + } + } + + stop() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + async syncNow() { + if (this.syncing) return this.status; + const config = getRemoteSyncConfig(); + if (!config?.serverUrl) { + this.updateStatus({ connected: false, syncing: false }); + return this.status; + } + + const remoteJwt = getRemoteSyncJwt(); + if (!remoteJwt) { + this.updateStatus({ + connected: true, + syncing: false, + needsReauth: true, + lastError: "Not signed in to remote server", + }); + return this.status; + } + if (isJwtExpiredOrExpiringSoon(remoteJwt)) { + this.updateStatus({ + connected: true, + syncing: false, + needsReauth: true, + lastError: "Remote session expired", + }); + return this.status; + } + if (!this.localJwt) { + // Local login hasn't handed us a token yet -- this is expected for the + // first tick or two right after a cold boot (renderer hasn't finished + // its own session check yet), but if it never arrives (e.g. a gap in + // whichever code path establishes the local session), sync would + // otherwise silently no-op forever with no visible error. Surface it + // as a normal, non-alarming "not synced yet" status rather than + // leaving lastSyncedAt/lastError untouched. + this.updateStatus({ + connected: true, + syncing: false, + lastError: "Waiting for local session", + }); + return this.status; + } + + this.syncing = true; + this.updateStatus({ connected: true, syncing: true, lastError: null }); + + try { + const state = readJson(getRemoteSyncStatePath(), { entities: {} }); + let sawAuthFailure = false; + + for (const entityType of SYNCED_ENTITY_TYPES) { + const entityState = state.entities[entityType] || { + lastPulledAt: null, + lastPushedAt: null, + }; + + const result = await this.syncEntity({ + entityType, + remoteBaseUrl: config.serverUrl.replace(/\/$/, ""), + remoteJwt, + since: entityState.lastPulledAt, + }); + + if (result.authFailure) { + sawAuthFailure = true; + break; + } + + state.entities[entityType] = { + lastPulledAt: result.syncedAt, + lastPushedAt: result.syncedAt, + }; + } + + if (sawAuthFailure) { + this.updateStatus({ + syncing: false, + needsReauth: true, + lastError: "Remote server rejected the session", + }); + return this.status; + } + + writeJson(getRemoteSyncStatePath(), state); + writeJson(getRemoteSyncConfigPath(), { + ...config, + lastSyncedAt: new Date().toISOString(), + lastSyncStatus: "ok", + lastSyncError: null, + }); + + this.updateStatus({ + connected: true, + syncing: false, + needsReauth: false, + lastSyncedAt: new Date().toISOString(), + lastError: null, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + writeJson(getRemoteSyncConfigPath(), { + ...config, + lastSyncStatus: "error", + lastSyncError: message, + }); + this.updateStatus({ syncing: false, lastError: message }); + } finally { + this.syncing = false; + } + + return this.status; + } + + async fetchJson(url, token, options = {}) { + const res = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + ...(options.headers || {}), + }, + }); + if (res.status === 401 || res.status === 403) { + const err = new Error(`Auth failed (${res.status})`); + err.authFailure = true; + throw err; + } + if (!res.ok) { + throw new Error(`Request failed (${res.status}): ${url}`); + } + + const text = await res.text(); + // A reverse-proxy SSO in front of the remote server (Pangolin, Authelia, + // etc.) can intercept even an authenticated, Bearer-token'd request and + // serve its own login page instead of forwarding to Termix -- that comes + // back as a normal 200 OK, so the status checks above don't catch it. + // This is NOT the same as needsReauth/a bad Termix JWT: sync runs as a + // plain server-to-server fetch() in this main process, with no browser + // cookie jar at all, so re-authenticating through the login iframe (which + // only affects the renderer's browser session) can never fix this -- + // reconnecting would tell the user to do something that doesn't help. + // The proxy has to allow this traffic through some other way (an API + // bypass rule, a separate hostname/port that isn't proxy-gated, etc.), + // so this gets its own distinct, honest error rather than piggybacking + // on needsReauth or a raw JSON.parse crash. + const looksLikeHtml = + text.includes("") || + text.includes(""); + if (looksLikeHtml) { + const err = new Error( + "The reverse proxy in front of this server is blocking sync traffic with its own login page. Reconnecting won't fix this. The proxy needs to let Termix's API requests through (e.g. an SSO bypass rule for the sync API, or a non-proxied hostname/port for it).", + ); + err.proxyBlocked = true; + throw err; + } + + try { + return JSON.parse(text); + } catch { + throw new Error(`Server returned invalid JSON: ${url}`); + } + } + + async pullSide(baseUrl, token, entityType, since) { + const url = `${baseUrl}/sync/${entityType}${since ? `?since=${encodeURIComponent(since)}` : ""}`; + const data = await this.fetchJson(url, token); + return data.rows || []; + } + + /** + * Every syncId a side currently holds, ignoring the incremental window. + * Used only to decide whether a deletion still has something to delete. + */ + async pullSyncIds(baseUrl, token, entityType) { + const rows = await this.pullSide(baseUrl, token, entityType, null); + return new Set(rows.filter((row) => row.syncId).map((row) => row.syncId)); + } + + async pullTombstones(baseUrl, token, entityType, since) { + const url = `${baseUrl}/sync/${entityType}/tombstones${since ? `?since=${encodeURIComponent(since)}` : ""}`; + const data = await this.fetchJson(url, token); + return data.tombstones || []; + } + + async pushRow(baseUrl, token, entityType, row) { + await this.fetchJson(`${baseUrl}/sync/${entityType}`, token, { + method: "POST", + body: JSON.stringify({ row }), + }); + } + + async pushTombstone(baseUrl, token, entityType, syncId) { + await this.fetchJson(`${baseUrl}/sync/tombstones`, token, { + method: "POST", + body: JSON.stringify({ entityType, syncId }), + }); + } + + async syncEntity({ entityType, remoteBaseUrl, remoteJwt, since }) { + const syncedAt = new Date().toISOString(); + try { + const [localRows, remoteRows, localTombstones, remoteTombstones] = + await Promise.all([ + this.pullSide(EMBEDDED_BASE_URL, this.localJwt, entityType, since), + this.pullSide(remoteBaseUrl, remoteJwt, entityType, since), + this.pullTombstones( + EMBEDDED_BASE_URL, + this.localJwt, + entityType, + since, + ), + this.pullTombstones(remoteBaseUrl, remoteJwt, entityType, since), + ]); + + const tombstonedSyncIds = new Set([ + ...localTombstones.map((t) => t.syncId), + ...remoteTombstones.map((t) => t.syncId), + ]); + + const localBySyncId = new Map( + localRows.filter((r) => r.syncId).map((r) => [r.syncId, r]), + ); + const remoteBySyncId = new Map( + remoteRows.filter((r) => r.syncId).map((r) => [r.syncId, r]), + ); + const allSyncIds = new Set([ + ...localBySyncId.keys(), + ...remoteBySyncId.keys(), + ]); + + for (const syncId of allSyncIds) { + if (tombstonedSyncIds.has(syncId)) continue; + + const localRow = localBySyncId.get(syncId); + const remoteRow = remoteBySyncId.get(syncId); + + if (localRow && !remoteRow) { + await this.pushRow(remoteBaseUrl, remoteJwt, entityType, localRow); + } else if (remoteRow && !localRow) { + await this.pushRow( + EMBEDDED_BASE_URL, + this.localJwt, + entityType, + remoteRow, + ); + } else if (localRow && remoteRow) { + const localUpdatedAt = new Date(localRow.updatedAt || 0).getTime(); + const remoteUpdatedAt = new Date(remoteRow.updatedAt || 0).getTime(); + if (localUpdatedAt > remoteUpdatedAt) { + await this.pushRow(remoteBaseUrl, remoteJwt, entityType, localRow); + } else if (remoteUpdatedAt > localUpdatedAt) { + await this.pushRow( + EMBEDDED_BASE_URL, + this.localJwt, + entityType, + remoteRow, + ); + } + } + } + + // Apply tombstones to whichever side hasn't already deleted the row. + // + // The presence check cannot use localRows/remoteRows: those are the + // incremental window, and a row deleted on one side while untouched on + // the other is by definition outside it, so every deletion was dropped. + // It also cannot be skipped -- pushing unconditionally makes the + // receiving side record a fresh tombstone, which the next pass would push + // back, forever. So ask the receiving side what it actually still holds, + // and only when there is a deletion to apply. + if (localTombstones.length) { + const remoteSyncIds = await this.pullSyncIds( + remoteBaseUrl, + remoteJwt, + entityType, + ); + for (const tombstone of localTombstones) { + if (remoteSyncIds.has(tombstone.syncId)) { + await this.pushTombstone( + remoteBaseUrl, + remoteJwt, + entityType, + tombstone.syncId, + ); + } + } + } + if (remoteTombstones.length) { + const localSyncIds = await this.pullSyncIds( + EMBEDDED_BASE_URL, + this.localJwt, + entityType, + ); + for (const tombstone of remoteTombstones) { + if (localSyncIds.has(tombstone.syncId)) { + await this.pushTombstone( + EMBEDDED_BASE_URL, + this.localJwt, + entityType, + tombstone.syncId, + ); + } + } + } + + return { syncedAt }; + } catch (error) { + if (error?.authFailure) { + return { syncedAt, authFailure: true }; + } + throw error; + } + } +} + +let engine = null; + +function initRemoteSync(getMainWindow) { + engine = new RemoteSyncEngine(getMainWindow); + engine.start(); + return engine; +} + +function getRemoteSyncEngine() { + return engine; +} + +module.exports = { + initRemoteSync, + getRemoteSyncEngine, + getDesktopSettings, + saveDesktopSettings, + getRemoteSyncConfig, + saveRemoteSyncConfig, + clearRemoteSyncConfig, + saveRemoteSyncJwt, + getRemoteSyncJwt, + clearRemoteSyncJwt, + getRemoteSyncUserInfo, + isJwtExpiredOrExpiringSoon, + decodeJwtExpiry, +}; diff --git a/eslint.config.mjs b/eslint.config.mjs index ee87445..7879884 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -46,4 +46,57 @@ export default tseslint.config([ "react-refresh/only-export-components": "warn", }, }, + { + // MySQL has no RETURNING clause, and drizzle's mysql-core does not expose + // the method at all โ€” a bare .returning() is a TypeError there, not a bad + // query, and it only fails on the engine no test in this repo runs against. + // + // 175 call sites were migrated off it. This is what stops number 176. + // Writes that need rows back go through repositories/returning.ts, which + // picks one statement or a read-then-write transaction per dialect. + files: ["src/backend/database/repositories/**/*.ts"], + ignores: [ + // The two files whose job is to absorb these differences. + "src/backend/database/repositories/returning.ts", + "src/backend/database/repositories/mutation-result.ts", + ], + rules: { + "no-restricted-syntax": [ + "error", + { + selector: "CallExpression[callee.property.name='returning']", + message: + "MySQL has no RETURNING. Use insertReturning/updateReturning/deleteReturning from ./returning.js, or rowsAffected() if you only need a count. Inside a proven sqlite-only branch, disable this rule with a comment saying so.", + }, + { + // `||` concatenates on SQLite and Postgres. On MySQL it is logical OR + // unless the server runs with PIPES_AS_CONCAT, so a folder path built + // this way silently became 0. Use CONCAT, which all three agree on. + selector: + "TaggedTemplateExpression[tag.name='sql'] TemplateElement[value.raw=/\\|\\|/]", + message: + "`||` is logical OR on MySQL, not concatenation. Use CONCAT(...).", + }, + { + // Postgres and SQLite spell it ON CONFLICT; MySQL spells it ON + // DUPLICATE KEY and names no columns, so drizzle's mysql-core has no + // onConflictDoUpdate at all โ€” another TypeError, not a bad query. + selector: "CallExpression[callee.property.name='onConflictDoUpdate']", + message: + "MySQL has no ON CONFLICT. Use upsert() from ./returning.js.", + }, + { + // better-sqlite3 puts these on a write result; node-postgres and + // mysql2 do not, so reading them directly yields undefined โ€” and + // Number(undefined) is NaN, which reaches the database as the string + // "NaN" and fails an integer column. Three call sites did exactly + // this and only broke on Postgres. + selector: + "MemberExpression[property.name=/^(lastInsertRowid|changes)$/]", + message: + "lastInsertRowid and changes are better-sqlite3 only. Use insertedId() or rowsAffected() from ./mutation-result.js.", + }, + ], + }, + }, ]); diff --git a/index.html b/index.html index da71ea3..f3f0c8f 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,10 @@ - + @@ -44,6 +47,24 @@ border-radius: 3px; border: 1px solid #1e1e21; } + + .toolbar-scrollbar { + scrollbar-width: thin; + scrollbar-color: #4a4a4a transparent; + } + + .toolbar-scrollbar::-webkit-scrollbar { + height: 3px; + } + + .toolbar-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + + .toolbar-scrollbar::-webkit-scrollbar-thumb { + background-color: #4a4a4a; + border-radius: 2px; + } diff --git a/package-lock.json b/package-lock.json index 4e9b87b..734f113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,154 +1,162 @@ { "name": "termix", - "version": "2.5.1", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "termix", - "version": "2.5.1", + "version": "2.7.0", "hasInstallScript": true, "dependencies": { + "@anthropic-ai/sdk": "^0.116.0", "@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/server": "^13.3.2", - "@tanstack/react-virtual": "^3.14.6", + "@tanstack/react-virtual": "^3.14.9", + "@types/compression": "^1.8.1", "@types/ldapjs": "^3.0.6", - "axios": "^1.18.1", + "axios": "^1.19.0", "bcryptjs": "^3.0.3", - "better-sqlite3": "^12.11.1", + "better-sqlite3": "^13.0.2", "body-parser": "^2.3.0", - "chalk": "^5.6.2", + "chalk": "^6.0.0", + "compression": "^1.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "express": "^5.2.1", "guacamole-lite": "^1.2.0", - "jose": "^6.2.2", - "js-yaml": "^5.2.1", + "jose": "^6.2.8", + "js-yaml": "^5.2.3", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", "ldapjs": "^3.0.7", - "motion": "^12.42.2", + "motion": "^12.43.0", "multer": "^2.2.0", - "nanoid": "^5.1.16", + "mysql2": "^3.23.2", + "nanoid": "^6.0.1", + "node-pty": "^1.1.0", + "pg": "^8.22.0", "qrcode": "^1.5.4", "serialport": "^13.0.0", + "sharp": "^0.35.3", "socks": "^2.8.7", "speakeasy": "^2.0.0", "ssh2": "^1.17.0", - "undici": "^8.7.0", - "ws": "^8.20.0" + "undici": "^8.10.0", + "ws": "^8.21.1" }, "devDependencies": { - "@biomejs/biome": "2.5.2", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.10.4", "@codemirror/search": "^6.7.1", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.5", - "@commitlint/cli": "^21.0.2", - "@commitlint/config-conventional": "^21.0.2", + "@codemirror/view": "^6.43.7", + "@commitlint/cli": "^21.2.1", + "@commitlint/config-conventional": "^21.2.0", "@deadendjs/swagger-jsdoc": "^8.1.2", "@electron/notarize": "^3.1.1", - "@electron/rebuild": "^4.0.4", + "@electron/rebuild": "^4.2.0", "@eslint/js": "^10.0.1", - "@fontsource-variable/jetbrains-mono": "^5.2.8", - "@fontsource/fira-code": "^5.2.7", - "@fontsource/jetbrains-mono": "^5.2.8", - "@fontsource/source-code-pro": "^5.2.7", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource/fira-code": "^5.3.0", + "@fontsource/jetbrains-mono": "^5.3.0", + "@fontsource/source-code-pro": "^5.3.0", "@monaco-editor/react": "^4.7.0", - "@radix-ui/react-accordion": "^1.2.15", - "@radix-ui/react-alert-dialog": "^1.1.18", - "@radix-ui/react-checkbox": "^1.3.6", - "@radix-ui/react-dialog": "^1.1.18", - "@radix-ui/react-dropdown-menu": "^2.1.19", - "@radix-ui/react-label": "^2.1.11", - "@radix-ui/react-popover": "^1.1.18", - "@radix-ui/react-progress": "^1.1.11", - "@radix-ui/react-scroll-area": "^1.2.13", - "@radix-ui/react-select": "^2.3.2", - "@radix-ui/react-separator": "^1.1.11", - "@radix-ui/react-slider": "^1.4.2", - "@radix-ui/react-slot": "^1.3.0", - "@radix-ui/react-switch": "^1.3.2", - "@radix-ui/react-tabs": "^1.1.16", - "@radix-ui/react-tooltip": "^1.2.11", - "@tailwindcss/vite": "^4.3.2", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-alert-dialog": "^1.1.23", + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-popover": "^1.1.23", + "@radix-ui/react-progress": "^1.1.16", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slider": "^1.4.7", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-switch": "^1.3.7", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tailwindcss/vite": "^4.3.3", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@types/better-sqlite3": "^7.6.13", + "@types/better-sqlite3": "^9.6.0", "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/guacamole-common-js": "^1.5.5", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/multer": "^2.1.0", - "@types/node": "^26.0.0", + "@types/multer": "^2.2.0", + "@types/node": "^26.1.2", + "@types/pg": "^8.20.3", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@types/speakeasy": "^2.0.10", "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", - "@uiw/codemirror-extensions-langs": "^4.25.9", - "@uiw/codemirror-theme-github": "^4.25.9", - "@uiw/react-codemirror": "^4.25.9", - "@vitejs/plugin-react": "^6.0.3", - "@vitest/coverage-v8": "^4.1.9", - "@vitest/ui": "^4.1.9", + "@uiw/codemirror-extensions-langs": "^4.25.11", + "@uiw/codemirror-theme-github": "^4.25.11", + "@uiw/react-codemirror": "^4.25.11", + "@vitejs/plugin-react": "^6.0.5", + "@vitest/coverage-v8": "^4.1.10", + "@vitest/ui": "^4.1.10", "@xterm/addon-clipboard": "^0.2.0", "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-search": "^0.16.0", "@xterm/addon-unicode11": "^0.9.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "concurrently": "^10.0.3", + "concurrently": "^10.0.4", "cytoscape": "^3.34.0", - "electron": "^43.0.0", + "drizzle-kit": "^0.31.10", + "electron": "^43.2.0", "electron-builder": "^26.15.3", - "eslint": "^10.5.0", + "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "eslint-plugin-unused-imports": "^4.4.1", - "globals": "^17.5.0", + "globals": "^17.8.0", "guacamole-common-js": "^1.5.0", "husky": "^9.1.7", - "i18next": "^26.3.4", + "i18next": "^26.3.6", "i18next-browser-languagedetector": "^8.2.1", - "jsdom": "^29.1.1", - "lint-staged": "^17.0.8", - "lucide-react": "^1.20.0", - "prettier": "3.8.4", - "radix-ui": "^1.6.1", - "react": "^19.2.7", + "jsdom": "^30.0.1", + "lint-staged": "^17.2.0", + "lucide-react": "^1.28.0", + "prettier": "3.9.6", + "radix-ui": "^1.6.7", + "react": "^19.2.8", "react-cytoscapejs": "^2.0.0", - "react-dom": "^19.2.7", + "react-dom": "^19.2.8", "react-h5-audio-player": "^3.10.2", - "react-hook-form": "^7.79.0", - "react-i18next": "^17.0.4", - "react-icons": "^5.6.0", + "react-hook-form": "^7.84.0", + "react-i18next": "^17.0.11", + "react-icons": "^5.7.0", "react-markdown": "^10.1.0", "react-pdf": "^10.4.1", "react-photo-view": "^1.2.7", "react-syntax-highlighter": "^16.1.1", "react-xtermjs": "^1.0.10", "remark-gfm": "^4.0.1", - "sharp": "^0.35.3", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", "tw-animate-css": "^1.4.0", "typescript": "~6.0.3", - "typescript-eslint": "^8.61.1", - "vite": "^8.0.16", + "typescript-eslint": "^8.66.0", + "vite": "^8.2.0", "vite-plugin-svgr": "^5.2.0", - "vitest": "^4.1.9" + "vitest": "^4.1.10" }, "engines": { "node": ">=22.12.0", @@ -162,6 +170,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.116.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.116.0.tgz", + "integrity": "sha512-4UEapYQ+epLEMsAuLZDvW8ExVSOtHD8a7zTyLzhw0H9RXJ1eilPgmqhjwgcdg22diwx13spw6fJ4rONZ+bS7Ww==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", @@ -180,9 +209,9 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -238,56 +267,58 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz", + "integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-calc": "^3.2.1", + "@csstools/css-color-parser": "^4.1.9", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "20 || >=22" } }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -504,7 +535,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -568,169 +598,6 @@ "node": ">=18" } }, - "node_modules/@biomejs/biome": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.2.tgz", - "integrity": "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.2", - "@biomejs/cli-darwin-x64": "2.5.2", - "@biomejs/cli-linux-arm64": "2.5.2", - "@biomejs/cli-linux-arm64-musl": "2.5.2", - "@biomejs/cli-linux-x64": "2.5.2", - "@biomejs/cli-linux-x64-musl": "2.5.2", - "@biomejs/cli-win32-arm64": "2.5.2", - "@biomejs/cli-win32-x64": "2.5.2" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.2.tgz", - "integrity": "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.2.tgz", - "integrity": "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.2.tgz", - "integrity": "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.2.tgz", - "integrity": "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.2.tgz", - "integrity": "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.2.tgz", - "integrity": "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.2.tgz", - "integrity": "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.2.tgz", - "integrity": "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1129,9 +996,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.43.5", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.5.tgz", - "integrity": "sha512-7uT/vUgH6dfXWn3WqOe23KneILMvGy5wQjNMEcRXLKzziJ9NOktpW6tGoyQpwVkBgE5Gj6hKkCcsddbnkaWrOQ==", + "version": "6.43.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.7.tgz", + "integrity": "sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==", "dev": true, "license": "MIT", "dependencies": { @@ -1142,17 +1009,18 @@ } }, "node_modules/@commitlint/cli": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.0.2.tgz", - "integrity": "sha512-YMmfLbqBg+ZRvvmPhc+cilSQFrh/AgzVgCT1U/OifmUZEwPbvCtA8rN//YNaF9d5eoZphxVMGYtmwA2QgQORgg==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.2.1.tgz", + "integrity": "sha512-blsZGe29hJ72VGEFVl72IVYX+1vsfINpjA9yWQA6i7OKD/McGEOXg08sKIRKjFk4JvzhV/9n0l3i6NooPLTNfg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/format": "^21.0.1", - "@commitlint/lint": "^21.0.2", - "@commitlint/load": "^21.0.2", - "@commitlint/read": "^21.0.2", - "@commitlint/types": "^21.0.1", + "@commitlint/config-conventional": "^21.2.0", + "@commitlint/format": "^21.2.0", + "@commitlint/lint": "^21.2.0", + "@commitlint/load": "^21.2.0", + "@commitlint/read": "^21.2.1", + "@commitlint/types": "^21.2.0", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, @@ -1263,27 +1131,27 @@ } }, "node_modules/@commitlint/config-conventional": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.0.2.tgz", - "integrity": "sha512-P/ZRhryQmkj0Z0dY9FOoRwe3xkwJyyAdtXwt01NT2kuZttcG2CNYp1q5Ci3u+nDT2jcbJRw2kt13Czl1qKNPfg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.2.0.tgz", + "integrity": "sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-conventionalcommits": "^9.2.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-conventionalcommits": "^10.0.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/config-validator": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.0.1.tgz", - "integrity": "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.2.0.tgz", + "integrity": "sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "ajv": "^8.11.0" }, "engines": { @@ -1291,13 +1159,13 @@ } }, "node_modules/@commitlint/ensure": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.0.1.tgz", - "integrity": "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.2.0.tgz", + "integrity": "sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0" }, "engines": { @@ -1315,13 +1183,13 @@ } }, "node_modules/@commitlint/format": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.0.1.tgz", - "integrity": "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.2.0.tgz", + "integrity": "sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "picocolors": "^1.1.1" }, "engines": { @@ -1329,13 +1197,13 @@ } }, "node_modules/@commitlint/is-ignored": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.0.2.tgz", - "integrity": "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.2.0.tgz", + "integrity": "sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "semver": "^7.6.0" }, "engines": { @@ -1343,32 +1211,32 @@ } }, "node_modules/@commitlint/lint": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.0.2.tgz", - "integrity": "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.2.0.tgz", + "integrity": "sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/is-ignored": "^21.0.2", - "@commitlint/parse": "^21.0.2", - "@commitlint/rules": "^21.0.2", - "@commitlint/types": "^21.0.1" + "@commitlint/is-ignored": "^21.2.0", + "@commitlint/parse": "^21.2.0", + "@commitlint/rules": "^21.2.0", + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/load": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.0.2.tgz", - "integrity": "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.2.0.tgz", + "integrity": "sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", "@commitlint/execute-rule": "^21.0.1", - "@commitlint/resolve-extends": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/resolve-extends": "^21.2.0", + "@commitlint/types": "^21.2.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", @@ -1380,9 +1248,9 @@ } }, "node_modules/@commitlint/message": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.0.2.tgz", - "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.2.0.tgz", + "integrity": "sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==", "dev": true, "license": "MIT", "engines": { @@ -1390,30 +1258,30 @@ } }, "node_modules/@commitlint/parse": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.0.2.tgz", - "integrity": "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.2.0.tgz", + "integrity": "sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-angular": "^9.0.0", + "conventional-commits-parser": "^7.0.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/read": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.0.2.tgz", - "integrity": "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.2.1.tgz", + "integrity": "sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/top-level": "^21.0.2", - "@commitlint/types": "^21.0.1", - "git-raw-commits": "^5.0.0", + "@commitlint/top-level": "^21.2.0", + "@commitlint/types": "^21.2.0", + "@conventional-changelog/git-client": "^3.0.0", "tinyexec": "^1.0.0" }, "engines": { @@ -1421,14 +1289,14 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", - "integrity": "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.2.0.tgz", + "integrity": "sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" @@ -1438,16 +1306,16 @@ } }, "node_modules/@commitlint/rules": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.0.2.tgz", - "integrity": "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.2.0.tgz", + "integrity": "sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^21.0.1", - "@commitlint/message": "^21.0.2", + "@commitlint/ensure": "^21.2.0", + "@commitlint/message": "^21.2.0", "@commitlint/to-lines": "^21.0.1", - "@commitlint/types": "^21.0.1" + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" @@ -1464,9 +1332,9 @@ } }, "node_modules/@commitlint/top-level": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.0.2.tgz", - "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.2.0.tgz", + "integrity": "sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1477,13 +1345,13 @@ } }, "node_modules/@commitlint/types": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.0.1.tgz", - "integrity": "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.2.0.tgz", + "integrity": "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==", "dev": true, "license": "MIT", "dependencies": { - "conventional-commits-parser": "^6.3.0", + "conventional-commits-parser": "^7.0.0", "picocolors": "^1.1.1" }, "engines": { @@ -1491,22 +1359,22 @@ } }, "node_modules/@conventional-changelog/git-client": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", - "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.1.0.tgz", + "integrity": "sha512-Tqa/gHco2WJWa740NRjOrfKVvzIqxkZpecb8bemaQ8sKM5PXb1UK4uTyTb/1wIqNuOVaDOFxyBdhTIQZn6gdjQ==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.2.0", + "@simple-libs/child-process-utils": "^2.0.0", + "@simple-libs/stream-utils": "^2.0.0", "semver": "^7.5.2" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.4.0" + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.0.1" }, "peerDependenciesMeta": { "conventional-commits-filter": { @@ -1517,10 +1385,20 @@ } } }, + "node_modules/@conventional-changelog/template": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.2.1.tgz", + "integrity": "sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", "dev": true, "funding": [ { @@ -1538,9 +1416,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -1562,9 +1440,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", - "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", "dev": true, "funding": [ { @@ -1578,8 +1456,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.1" + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -1613,9 +1491,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", - "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", "dev": true, "funding": [ { @@ -1676,6 +1554,13 @@ "node": ">=20.0.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@electron-internal/extract-zip": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz", @@ -1828,9 +1713,9 @@ } }, "node_modules/@electron/get/node_modules/undici": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", - "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "optional": true, @@ -1888,9 +1773,9 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1935,9 +1820,9 @@ "license": "MIT" }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -1975,38 +1860,470 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" } }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2054,9 +2371,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2185,9 +2502,9 @@ "license": "MIT" }, "node_modules/@fontsource-variable/jetbrains-mono": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", - "integrity": "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==", "dev": true, "license": "OFL-1.1", "funding": { @@ -2195,9 +2512,9 @@ } }, "node_modules/@fontsource/fira-code": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.7.tgz", - "integrity": "sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.3.0.tgz", + "integrity": "sha512-EJL968RJRkakubAj/coU8pSUaeTE5UNoRjtzAr6kGiSZ3jWuN8/AKWHwym/PFUaQL1q7IL/H+EXs4358YhrTBQ==", "dev": true, "license": "OFL-1.1", "funding": { @@ -2205,9 +2522,9 @@ } }, "node_modules/@fontsource/jetbrains-mono": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", - "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", "dev": true, "license": "OFL-1.1", "funding": { @@ -2215,9 +2532,9 @@ } }, "node_modules/@fontsource/source-code-pro": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/source-code-pro/-/source-code-pro-5.2.7.tgz", - "integrity": "sha512-7papq9TH94KT+S5VSY8cU7tFmwuGkIe3qxXRMscuAXH6AjMU+KJI75f28FzgBVDrlMfA0jjlTV4/x5+H5o/5EQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/source-code-pro/-/source-code-pro-5.3.0.tgz", + "integrity": "sha512-JxaCODU69HDS3mVra9u96nyBF911La6IvtGLgpQD+PZLxJ1i9IxooNfLR6Y37kx06IFjVGkvoUmg3WPwh/8gBg==", "dev": true, "license": "OFL-1.1", "funding": { @@ -2323,7 +2640,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2336,7 +2652,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2359,7 +2674,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2379,7 +2693,6 @@ "version": "0.35.3", "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2402,7 +2715,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2419,7 +2731,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2436,7 +2747,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2453,7 +2763,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2470,7 +2779,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2487,7 +2795,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2504,7 +2811,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2521,7 +2827,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2538,7 +2843,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2555,7 +2859,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2572,7 +2875,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2595,7 +2897,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2618,7 +2919,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2641,7 +2941,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2664,7 +2963,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2687,7 +2985,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2710,7 +3007,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2733,7 +3029,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2753,7 +3048,6 @@ "version": "0.35.3", "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -2770,7 +3064,6 @@ "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2784,7 +3077,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2804,7 +3096,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2824,7 +3115,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2844,7 +3134,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3574,25 +3863,6 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@noble/hashes": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", @@ -3607,9 +3877,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", "funding": { @@ -3822,27 +4092,27 @@ "license": "MIT" }, "node_modules/@radix-ui/number": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", - "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", "dev": true, "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", - "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", "dev": true, "license": "MIT" }, "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.11.tgz", - "integrity": "sha512-HQDOFTKwSnmUij6l54wYJJtxTAnxI71+YJLOrjm2ladFB8HAV5Jt7hwaZPhWTGBkYoW4+ZAOfNZrLDh/qvxSYA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.15.tgz", + "integrity": "sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -3860,21 +4130,21 @@ } }, "node_modules/@radix-ui/react-accordion": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.15.tgz", - "integrity": "sha512-24Zz/0SYx8F2bSVThBnQrdJs2VbKelyuJordcFRRdA0fRAhrq/wSegGCqaQz34VQoiWqSMGYCYXEhynLSlyQlg==", + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", + "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collapsible": "1.1.15", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -3892,17 +4162,17 @@ } }, "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.18.tgz", - "integrity": "sha512-6c2cXpNlAgHDhKguK24XcWHHayMpK+lk7/WwBXBco+ZJ4Dv7xP++GBM280KgTD/HCRu3jSdfe8WQiZssonYaIA==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.23.tgz", + "integrity": "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dialog": "1.1.18", - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -3920,13 +4190,13 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.11.tgz", - "integrity": "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -3944,13 +4214,13 @@ } }, "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.11.tgz", - "integrity": "sha512-IUAhIVpBUvP5NNICjlaB1OFmtRLGqQqTF3ZOSGPoq3XeLXRFtHiWTRxSVEULgOd9GQR2c7tsYqDnhUennapZnw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.15.tgz", + "integrity": "sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -3968,17 +4238,18 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.1.tgz", - "integrity": "sha512-+8PWoLLZv3AVb5m0pvoiOca/bQGzc9vPVb+982HB2x3Un0DpYEPM3zLMl4oqRpBsocJuNqLkiv/HXTnTrlwr4g==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3996,20 +4267,19 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.6.tgz", - "integrity": "sha512-eUEUoGMDpfkgHWSE97ZZaUJtzR1M7EKnNIpD1Q16+8JR9NWghcaqMulx9PuCQ720w0UclfYn6FEbCdd5Hx087g==", + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4027,20 +4297,20 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.15.tgz", - "integrity": "sha512-8A1zibu5skAQ+UVbaeNH5hVMibiFCRJzgMuM14LTWGttnTZKQL9jwYnhAbHRuxrtCqPXa4JvvnVUq1pTNgyZYw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4058,16 +4328,16 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.11.tgz", - "integrity": "sha512-djW9+zeg137KQdlPtmE8xnaD+K2rcXXMWFrSg0hsmYZ6HRbdTA7tDHFgpaW9+huWVEu0RCabL+985T4TA0BE7g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -4085,9 +4355,9 @@ } }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4101,9 +4371,9 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", - "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4117,17 +4387,17 @@ } }, "node_modules/@radix-ui/react-context-menu": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.2.tgz", - "integrity": "sha512-qzsA/ZPhF6yMxBOTIk1nlCkoy2mswSbwYL+ErBa2iP0s4WWrlxmczArYqMcpVfEjmM7KJj/ADPXky0yZfbSxtQ==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.7.tgz", + "integrity": "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-menu": "2.1.19", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -4145,24 +4415,25 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.18.tgz", - "integrity": "sha512-apa28mldjMgORmE6g/w3sCcA0Y9UAVeeDVoozN4i7kOw12mLl9RBchfzK3Nn6qxOWjrZhK1Lfy7f07kyzxtnBw==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -4182,9 +4453,9 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4198,17 +4469,17 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.14.tgz", - "integrity": "sha512-4lUhWTWAjbDIqFrAPWJ3WqBOpO5YchVZ88X3nh6H9Lu5AFi5nCUeTPj3D8FSDmabmFeRe9ME0BDA4MwKTha5GQ==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-effect-event": "0.0.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", @@ -4226,19 +4497,19 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.19.tgz", - "integrity": "sha512-HZccBkbK0LOi8nYKIp5jll/zIRW0cCOmG6WWyqsSpmXCU+ZlcBbTqIwlBvPCu886C5RVu6c/kHV7xSP8IgYNHw==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.19", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -4256,9 +4527,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", - "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4272,15 +4543,15 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.11.tgz", - "integrity": "sha512-Mn88Vg2whaRocGJNOH+DKFqYm6ySFPQaiwHNxZPyjn99B52KAEJWWY9NP83+nWdk2HM3rdov+STu9AG471Rt9w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4298,18 +4569,18 @@ } }, "node_modules/@radix-ui/react-form": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.11.tgz", - "integrity": "sha512-0mTMJHv1gQAuEQoq5VDpTD3MRgmfUFdXAVFhpqR7wBeUr+tyRsof0wv/4XdPHLwQrefhoH2FiGHCggrCJhalIw==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.16.tgz", + "integrity": "sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -4327,21 +4598,21 @@ } }, "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.18.tgz", - "integrity": "sha512-rt+Fx4HoCeEwFL2IdoV2QaPltqDLlzxN77i9nwB3Y70scFlfAHh1QCdE2TXKuFJtA1TNygb0oivnFBZifgtZOw==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.23.tgz", + "integrity": "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -4359,13 +4630,13 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", - "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4378,13 +4649,13 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.11.tgz", - "integrity": "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==", + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -4402,28 +4673,28 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.19.tgz", - "integrity": "sha512-Mht9BVd1AIsNFVQr4KG3bIK7XQn5IXF0TL/2ObsrzOdc1loaly/+kBDL5roSCYn8j8XZkvpOD0WYLz2FQtH1Eg==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -4443,22 +4714,22 @@ } }, "node_modules/@radix-ui/react-menubar": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.19.tgz", - "integrity": "sha512-Glt6mebxcgQvLeVkH3HiqV5bgQubE+31ELxLs7q0GlYI5k0XYkOkeuPrhXoylxK8eufvIt9CJjzY1TfFMXK3qw==", + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.24.tgz", + "integrity": "sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.19", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -4476,26 +4747,26 @@ } }, "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.17.tgz", - "integrity": "sha512-fYeYQvbeNn5AQk2RBbpO7koLm2YbS00UYxC/IL2sgLlninEH5UNIv+X3E0KJ1Vy4WIo+dhN9w8GNqSHhbHWCIg==", + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.22.tgz", + "integrity": "sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -4513,24 +4784,24 @@ } }, "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.11.tgz", - "integrity": "sha512-Rsgab65u73E5kPVh8OS6PgPwJgPyf08GFfJDGAbMdF4DL7CgDhFOaDnXuk/DiMEVF6kgQwl0oJmFklvipmiOLg==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.16.tgz", + "integrity": "sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4548,20 +4819,20 @@ } }, "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.6.tgz", - "integrity": "sha512-pQ3xGp/uemomASPH97Eb3shfXX8QlG11bBJyEvRBV+vwtO4HvQlS06Yj9f31Ao7XepvF98SFrRgVDQ7jv+2xjQ==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.11.tgz", + "integrity": "sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3" }, "peerDependencies": { "@types/react": "*", @@ -4579,25 +4850,25 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.18.tgz", - "integrity": "sha512-qdXDes+eHlnMUGlBAAAe5EG7oOQvqsXuq4mq585diMudg80iB+jHbsSeG3+Q4eWNsogNyhqU2p/3i+Y0iEepqg==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -4617,22 +4888,22 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.2.tgz", - "integrity": "sha512-3QXNeMkdshed1MR3LNoiCirBywRFPkD8ETJa/HlPuLwSajaQixf2ro+isoDNJlGABg9ug41XuZpINZJIle4XWg==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", "dev": true, "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-rect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/rect": "1.1.2" + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", @@ -4650,14 +4921,14 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.13.tgz", - "integrity": "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==", + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4675,13 +4946,13 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", - "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4699,13 +4970,13 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", - "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -4723,14 +4994,14 @@ } }, "node_modules/@radix-ui/react-progress": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.11.tgz", - "integrity": "sha512-KqiGJcFaZDc+BvveAgU3ZhACg2MvSUDrCBx4lRR/ZVRNal0bvt8lBpvnSkep9heeOuF8Qfw3fszLDX4OpQ2NVw==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -4748,22 +5019,21 @@ } }, "node_modules/@radix-ui/react-radio-group": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.2.tgz", - "integrity": "sha512-W8Uo9riHnlzLLWy+r2mVHUyuEWqD/+be4PZzbEvaGoFSBDHkm+GYWjtcE6u3AmPKNyfanWpnVfpZ2GqPCdzzsw==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.7.tgz", + "integrity": "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4781,21 +5051,23 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.14.tgz", - "integrity": "sha512-8Qcnx9447tx/aCBgw6Jenfqg4Skq+vqab9mCBmuGNipIS5YXvL275wbKEu7+ICYHIlAPgCduUMJH1XOYewKF6Q==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4813,21 +5085,21 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.13.tgz", - "integrity": "sha512-7tncSubo2G0UY1e8rk+72qe3XRzrGnOLtZQ1PL1KoBfRUNX0NrJT5akb+0kfwSCc3gVR4wdHqyhAQBDpDNOwDw==", + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4845,32 +5117,32 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.2.tgz", - "integrity": "sha512-brXD6C/V0fVK0DDbscLVw6LsXrjQ+ay8jdOBaN+tLb4vsHsAMm6Gt6eT77wHX1Eq8GPtD5rJ+RxFtfDozsb4+Q==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7", + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -4890,13 +5162,13 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.11.tgz", - "integrity": "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -4914,23 +5186,23 @@ } }, "node_modules/@radix-ui/react-slider": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.2.tgz", - "integrity": "sha512-qt5C1ppJz66aUDrH1VccjPrq7aFchK0wBrn6xsxlCHNUyE57dRRQ7lp1QFpF7OscMexZF8MCGBTVBlENHPkNiA==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.7.tgz", + "integrity": "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4948,13 +5220,13 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3" + "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", @@ -4967,19 +5239,18 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.2.tgz", - "integrity": "sha512-tgRBI3DdNwAJYE4BBZyZcz/HRRCvAsPkRvG1wvKc+41tBGMxPn/a87T/wikXAvyDypNQ9kaZwHbeZe+veHCGpA==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4997,20 +5268,20 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.16.tgz", - "integrity": "sha512-v3Ab2l7z6U7tRB4xA0IyKdq0OsqaO1o9ZjsIEoKKnSZ/l96mZz8aCTX0NCXw+YVHJXr8Km4d+Mn6/Q8YjXa+gw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -5028,24 +5299,24 @@ } }, "node_modules/@radix-ui/react-toast": { - "version": "1.2.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.18.tgz", - "integrity": "sha512-YNEnTHV47hPep+U0QvVM02OJNka9uygREc+k4Nh5VSZBg4MmE+myI442x3hCGfRpX7N2WSSYSJKws4gE+Z8lgg==", + "version": "1.2.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.23.tgz", + "integrity": "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -5063,15 +5334,15 @@ } }, "node_modules/@radix-ui/react-toggle": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.13.tgz", - "integrity": "sha512-bI2ILJrzwgmAsH05TsJ9pVrzqQwAip7OM2/krqAdYn0R16bl86UPWbe5VPHsALat0EnqpV01cGtkleaUKPNdNg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -5089,19 +5360,19 @@ } }, "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.14.tgz", - "integrity": "sha512-TK1vusNKb8IRhF23FTbRgUNZ9zfs5rGIyI7LfR3h26p9LrQ060i0uW9QWeD8baZMddaaP0DBGlIa6pbZG+mitg==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-toggle": "1.1.13", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -5119,19 +5390,19 @@ } }, "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.14.tgz", - "integrity": "sha512-L/EkWVqlnj3lL2toHh4C7PwH2jxfa7OCq6lGfXSCii99ve2S4Ux5rc9HnOa7LN9exHa/Nl9kmCAmP9BuDPy5UA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.19.tgz", + "integrity": "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-toggle-group": "1.1.14" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-toggle-group": "1.1.19" }, "peerDependencies": { "@types/react": "*", @@ -5149,24 +5420,25 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.11.tgz", - "integrity": "sha512-8XZ6Py3y3W2nEzAUGCN5cfVKaUi+CVApcz1d6lrNVVf2hvYEixMRkq8k9ggPKnQUpRRuOV5avt8uvxViH2jLwA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -5184,9 +5456,9 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5200,14 +5472,15 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -5220,13 +5493,13 @@ } }, "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -5239,13 +5512,13 @@ } }, "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.3.tgz", - "integrity": "sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.5.tgz", + "integrity": "sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.2" + "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -5258,9 +5531,9 @@ } }, "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5274,9 +5547,9 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5290,9 +5563,9 @@ } }, "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", - "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5306,13 +5579,13 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", - "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.2" + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", @@ -5325,13 +5598,13 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", - "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -5344,13 +5617,13 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.7.tgz", - "integrity": "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -5368,9 +5641,9 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", - "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", "dev": true, "license": "MIT" }, @@ -5424,9 +5697,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", "cpu": [ "arm64" ], @@ -5441,9 +5714,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", "cpu": [ "arm64" ], @@ -5458,9 +5731,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", "cpu": [ "x64" ], @@ -5475,9 +5748,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", "cpu": [ "x64" ], @@ -5492,9 +5765,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", "cpu": [ "arm" ], @@ -5509,13 +5782,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -5526,13 +5802,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -5543,13 +5822,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -5560,13 +5842,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -5577,13 +5862,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -5594,13 +5882,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -5611,9 +5902,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", "cpu": [ "arm64" ], @@ -5627,29 +5918,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", "cpu": [ "arm64" ], @@ -5664,9 +5936,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", "cpu": [ "x64" ], @@ -5950,29 +6222,29 @@ } }, "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz", + "integrity": "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0" + "@simple-libs/stream-utils": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://ko-fi.com/dangreen" } }, "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://ko-fi.com/dangreen" @@ -6003,6 +6275,12 @@ "node": ">=20.0.0" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -6222,9 +6500,9 @@ } }, "node_modules/@svgr/core/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -6286,49 +6564,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", + "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ "arm64" ], @@ -6343,9 +6621,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -6360,9 +6638,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], @@ -6377,9 +6655,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], @@ -6394,9 +6672,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], @@ -6411,9 +6689,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], @@ -6428,9 +6706,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ "arm64" ], @@ -6445,9 +6723,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], @@ -6462,9 +6740,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], @@ -6479,9 +6757,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -6575,9 +6853,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -6592,9 +6870,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -6609,27 +6887,27 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "node_modules/@tanstack/react-virtual": { - "version": "3.14.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.6.tgz", - "integrity": "sha512-4+Uq8m0/gzO4kMCHUEpTtGX1RnONK0C+g88b2ltwPMWUBiaVarBuWKoPJaz7gj1cKCVRAdyu+U8GcKhwCc2beA==", + "version": "3.14.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.9.tgz", + "integrity": "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.17.4" + "@tanstack/virtual-core": "3.17.7" }, "funding": { "type": "github", @@ -6641,9 +6919,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.4.tgz", - "integrity": "sha512-nGm5KteqxasUdThLc2izl6dHUqLv0LQj7Nuyo5gYalTPf/U8a9ermvsl7reT+6ioBW1l8WfpP/mcU338nLXpqw==", + "version": "3.17.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.7.tgz", + "integrity": "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA==", "license": "MIT", "funding": { "type": "github", @@ -6688,9 +6966,9 @@ "license": "MIT" }, "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", + "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", "dev": true, "license": "MIT", "dependencies": { @@ -6702,9 +6980,12 @@ "redent": "^3.0.0" }, "engines": { - "node": ">=14", + "node": ">=22", "npm": ">=6", "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" } }, "node_modules/@testing-library/react": { @@ -6749,17 +7030,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -6768,9 +7038,9 @@ "license": "MIT" }, "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-9.6.0.tgz", + "integrity": "sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==", "dev": true, "license": "MIT", "dependencies": { @@ -6781,7 +7051,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -6799,11 +7068,20 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -6874,7 +7152,6 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -6886,7 +7163,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -6926,7 +7202,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/js-yaml": { @@ -6981,9 +7256,9 @@ "license": "MIT" }, "node_modules/@types/multer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", - "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==", "dev": true, "license": "MIT", "dependencies": { @@ -6991,14 +7266,26 @@ } }, "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" } }, + "node_modules/@types/pg": { + "version": "8.20.3", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.3.tgz", + "integrity": "sha512-4Tvg+HO6+oQaAkpT8GTYoSExzpGGZz532GXgbbCElWJQeQdMozBWxEKNBhJJpHFjWXsMxqPbyypvj/89FWNoSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/prismjs": { "version": "1.26.6", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", @@ -7020,20 +7307,18 @@ "version": "6.15.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "dev": true, "license": "MIT", "dependencies": { @@ -7041,9 +7326,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -7054,7 +7339,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -7064,7 +7348,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -7126,17 +7409,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", - "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/type-utils": "8.61.1", - "@typescript-eslint/utils": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -7149,15 +7432,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.61.1", + "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", "engines": { @@ -7165,16 +7448,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", - "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -7190,14 +7473,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", - "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.1", - "@typescript-eslint/types": "^8.61.1", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -7212,14 +7495,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", - "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7230,9 +7513,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", - "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -7247,15 +7530,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", - "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -7272,9 +7555,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", - "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -7286,16 +7569,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", - "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.61.1", - "@typescript-eslint/tsconfig-utils": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -7314,16 +7597,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", - "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1" + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7338,13 +7621,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", - "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -7356,9 +7639,9 @@ } }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.25.10", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.10.tgz", - "integrity": "sha512-P3vytLlpE62KYSWrMUnwDCv2lvaQDuDZzyj03mHntuHo5bSl34fRZpjTY3kQTPGuXHxkGSYpoPFFj+hMTqaaMQ==", + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.11.tgz", + "integrity": "sha512-otyFa+n9IOYtEjaKOxPedHkj15fTPUF21wdR9pv0GpZPfuGl27cvmcv6+tognbRu9VvEcsHKE+ESoszeo3KfTw==", "dev": true, "license": "MIT", "dependencies": { @@ -7384,9 +7667,9 @@ } }, "node_modules/@uiw/codemirror-extensions-langs": { - "version": "4.25.10", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.10.tgz", - "integrity": "sha512-VsfENMb23HrcKG2z0n0RB0KY7d11k+8qzUlH6i36099QXa07D/FEfeffoSnP+QdghhvISNnuMTJsWKqYQq6qlA==", + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.11.tgz", + "integrity": "sha512-RJ6MQTGInT+HBnzEs6PoUmPBgLFbWH3e9ZiF0+H1bdWJXrqN0Kfkiopumz1OCcbWFJ8aVJCD6KPK9mXV/LQ4yA==", "dev": true, "license": "MIT", "dependencies": { @@ -7425,22 +7708,22 @@ } }, "node_modules/@uiw/codemirror-theme-github": { - "version": "4.25.10", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.10.tgz", - "integrity": "sha512-iMM2QT4FaebJMO4W7lXmxNkRPIjKzgY26wL0QG0Ugy0gzsnxoNz4zgNeFIblPA8rvrN3vOIhNNh4nk9UOlFKxA==", + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.11.tgz", + "integrity": "sha512-3s0LK3gX2mvGI996z3G0tEHZqshbeJRly+QMRsKGAI1Tfr1V475bfaW9NvnoDdINuQHt+RB+ri6q57+WWF8d+A==", "dev": true, "license": "MIT", "dependencies": { - "@uiw/codemirror-themes": "4.25.10" + "@uiw/codemirror-themes": "4.25.11" }, "funding": { "url": "https://jaywcjlove.github.io/#/sponsor" } }, "node_modules/@uiw/codemirror-themes": { - "version": "4.25.10", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.10.tgz", - "integrity": "sha512-Fqiz1HIuDlDftcL+/O53V333UOH6MqQ84VbiQB5egn6u+uDwAqACp1FrdAoi4wgpR3b3TGW4Gr0wIYcrJSSz1A==", + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.11.tgz", + "integrity": "sha512-SBNCOgRsCtewGNocRbmjbCkltGXlFcPJsvhxQ351VynQjnWUiPbUrFcEU/haQ3HanROdAAjWXZJPk5bMBxl2jw==", "dev": true, "license": "MIT", "dependencies": { @@ -7458,9 +7741,9 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.25.10", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.10.tgz", - "integrity": "sha512-DzgSMwM5qzB7v1FIb4gEeriYt67iiay756/HIOM9mAbeOVK0MO7rqefHf0O5c0269pJKMW7AH9FjclExD23V9w==", + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.11.tgz", + "integrity": "sha512-DYVFAKLX+F/4JS9N/7xexh+TICrlncwkX9HKKInrP1bwO0tSfc3k0GB6oawTYhelVKh20cX3TuRx+NJSkVXuMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7468,7 +7751,7 @@ "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.25.10", + "@uiw/codemirror-extensions-basic-setup": "4.25.11", "codemirror": "^6.0.0" }, "funding": { @@ -7492,9 +7775,9 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { @@ -7518,14 +7801,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", - "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -7539,8 +7822,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.9", - "vitest": "4.1.9" + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -7549,16 +7832,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -7567,13 +7850,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -7604,9 +7887,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7617,13 +7900,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -7631,14 +7914,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -7647,9 +7930,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -7657,13 +7940,13 @@ } }, "node_modules/@vitest/ui": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.9.tgz", - "integrity": "sha512-U/cRvtqfEPj27FI1n9cyUvi4vXXdcLhjJiI+InYKdk8hP4VrS6RXOjGL7rfFaeBc37iRKANsR6eEzIoC7lmgBQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.10.tgz", + "integrity": "sha512-EOUqfXHTXtpSHsyLHH40ts3Ue+hRhSGwzwzMlK0dTEOLSDYyOXLyr5JDGmHQWhN2DYI30gw6dVx3cdgM9FZl+Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "fflate": "^0.8.2", "flatted": "^3.4.2", "pathe": "^2.0.3", @@ -7675,17 +7958,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.1.9" + "vitest": "4.1.10" } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -7720,6 +8003,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@xterm/addon-search": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0.tgz", + "integrity": "sha512-9OeuBFu0/uZJPu+9AHKY6g/w0Czyb/Ut0A5t79I4ULoU4IfU5BEpPFVGQxP4zTTMdfZEYkVIRYbHBX1xWwjeSA==", + "dev": true, + "license": "MIT" + }, "node_modules/@xterm/addon-unicode11": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0.tgz", @@ -7838,22 +8128,6 @@ } } }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -8010,9 +8284,9 @@ } }, "node_modules/app-builder-lib/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -8073,6 +8347,19 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/argue-cli": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/argue-cli/-/argue-cli-3.1.0.tgz", + "integrity": "sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -8096,13 +8383,6 @@ "node": ">= 0.4" } }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -8207,6 +8487,15 @@ "node": ">= 4.0.0" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aws4": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", @@ -8215,13 +8504,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -8251,20 +8540,6 @@ "node": ">= 6" } }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/backoff": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", @@ -8298,97 +8573,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.3.tgz", - "integrity": "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", - "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", - "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", - "license": "Apache-2.0", - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", - "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", - "license": "Apache-2.0", - "dependencies": { - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", - "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base32.js": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", @@ -8448,17 +8632,16 @@ } }, "node_modules/better-sqlite3": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", - "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.2.tgz", + "integrity": "sha512-jW6oufeDhXZaiX9Lw5A+oerVClx4iFrI6uDj1zu7SqUAjak9vbJvA0NEcKLNxHiQHb6kYCoFzzXYV0YOauhV3g==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" + "node-addon-api": "^8.0.0" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + "node": ">=22" } }, "node_modules/bidi-js": { @@ -8471,15 +8654,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -8525,16 +8699,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { @@ -8650,9 +8824,9 @@ } }, "node_modules/builder-util/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -8817,12 +8991,12 @@ } }, "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.0.tgz", + "integrity": "sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==", "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=22" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -8918,22 +9092,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -9096,17 +9254,6 @@ "node": ">=20" } }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -9117,6 +9264,60 @@ "node": ">=0.10.0" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -9154,15 +9355,15 @@ } }, "node_modules/concurrently": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", - "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.4.tgz", + "integrity": "sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", - "shell-quote": "1.8.4", + "shell-quote": "1.9.0", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" @@ -9191,6 +9392,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/concurrently/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/concurrently/node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -9300,46 +9514,46 @@ } }, "node_modules/conventional-changelog-angular": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", - "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.2.1.tgz", + "integrity": "sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.2.1" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-changelog-conventionalcommits": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", - "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.2.1.tgz", + "integrity": "sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.2.1" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-commits-parser": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", - "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz", + "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "meow": "^13.0.0" + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/convert-source-map": { @@ -9456,9 +9670,9 @@ } }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -9613,21 +9827,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -9653,6 +9852,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9727,9 +9935,9 @@ "license": "MIT" }, "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -9764,9 +9972,9 @@ } }, "node_modules/dmg-builder/node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -9804,19 +10012,6 @@ "tslib": "^2.0.3" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -9858,6 +10053,22 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, "node_modules/drizzle-orm": { "version": "0.45.2", "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", @@ -10039,9 +10250,9 @@ } }, "node_modules/electron": { - "version": "43.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-43.0.0.tgz", - "integrity": "sha512-PV60GsWU6qufhuOhw3n+Yix3WPDcqDtBqE8orbEQGQGHEkgp9o/JCPgb7L4vIL0r1HnfPdqSRtboOTqbDkcFDQ==", + "version": "43.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.2.0.tgz", + "integrity": "sha512-80zvrgG7ZRXD+tD0IyLvrnN9n+veSxadMRsMaC9wKKP3iUbtC7rGM8+dVuCmOb0Rrwwv8ESW4awnUZh9Hbp1fA==", "dev": true, "license": "MIT", "dependencies": { @@ -10200,19 +10411,10 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", "dev": true, "license": "MIT", "dependencies": { @@ -10246,19 +10448,6 @@ "node": ">=6" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -10329,16 +10518,59 @@ } }, "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", "dev": true, "license": "MIT", "workspaces": [ "docs", - "benchmarks" + "benchmarks", + "tests/types" ] }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -10369,9 +10601,9 @@ } }, "node_modules/eslint": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", - "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -10381,7 +10613,7 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", @@ -10405,7 +10637,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -10620,31 +10852,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -10737,12 +10944,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -10757,10 +10958,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -10826,12 +11033,6 @@ "node": ">=16.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -10850,9 +11051,9 @@ "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -10969,16 +11170,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -11024,12 +11225,12 @@ } }, "node_modules/framer-motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", - "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", + "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", "license": "MIT", "dependencies": { - "motion-dom": "^12.42.2", + "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, @@ -11098,6 +11299,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11177,29 +11387,19 @@ "node": ">= 0.4" } }, - "node_modules/git-raw-commits": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", - "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { - "@conventional-changelog/git-client": "^2.6.0", - "meow": "^13.0.0" + "resolve-pkg-maps": "^1.0.0" }, - "bin": { - "git-raw-commits": "src/cli.js" - }, - "engines": { - "node": ">=18" + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", @@ -11254,9 +11454,9 @@ } }, "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", "dev": true, "license": "MIT", "engines": { @@ -11343,9 +11543,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -11516,13 +11716,13 @@ "license": "MIT" }, "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz", + "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==", "dev": true, "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" + "funding": { + "url": "https://locize.com" } }, "node_modules/html-url-attributes": { @@ -11601,9 +11801,9 @@ } }, "node_modules/i18next": { - "version": "26.3.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz", - "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==", + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", "dev": true, "funding": [ { @@ -11621,7 +11821,7 @@ ], "license": "MIT", "peerDependencies": { - "typescript": "^5 || ^6" + "typescript": "^5 || ^6 || ^7" }, "peerDependenciesMeta": { "typescript": { @@ -11742,9 +11942,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "license": "MIT", "engines": { "node": ">= 12" @@ -11846,16 +12046,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -11882,6 +12072,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -12005,9 +12201,9 @@ } }, "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -12028,9 +12224,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz", - "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.3.tgz", + "integrity": "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==", "funding": [ { "type": "github", @@ -12050,39 +12246,39 @@ } }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", + "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", + "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^3.2.3" }, "peerDependenciesMeta": { "canvas": { @@ -12091,23 +12287,28 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/jsdom/node_modules/undici": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", - "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", "dev": true, "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, "engines": { - "node": ">=20.18.1" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/jsesc": { @@ -12137,6 +12338,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -12564,14 +12778,13 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz", - "integrity": "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.3.0.tgz", + "integrity": "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==", "dev": true, "license": "MIT", "dependencies": { - "listr2": "^10.2.1", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, @@ -12605,103 +12818,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/listr2": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", - "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.2.0", - "eventemitter3": "^5.0.4", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^10.0.0" - }, - "engines": { - "node": ">=22.13.0" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -12767,114 +12883,11 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/longest-streak": { "version": "3.1.0", @@ -12935,10 +12948,25 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lucide-react": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.20.0.tgz", - "integrity": "sha512-jhXLeC/7m0/tjL1nzMdKk6x256zWA6AtbhTVreHOiKPoeX2d6MK4FbyIQPpVq0E6iPWBisyy1TW+pEge/uMEuQ==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.28.0.tgz", + "integrity": "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==", "dev": true, "license": "ISC", "peerDependencies": { @@ -13347,19 +13375,6 @@ "node": ">= 0.8" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -14019,31 +14034,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -14074,6 +14064,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14102,19 +14093,13 @@ "node": ">= 18" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", - "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.43.0.tgz", + "integrity": "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==", "license": "MIT", "dependencies": { - "framer-motion": "^12.42.2", + "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -14135,9 +14120,9 @@ } }, "node_modules/motion-dom": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", - "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", + "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", "license": "MIT", "dependencies": { "motion-utils": "^12.39.0" @@ -14227,6 +14212,40 @@ "node": ">= 0.6" } }, + "node_modules/mysql2": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.2.tgz", + "integrity": "sha512-fxh3HpQ8vJtu/Mmnd4Xsur19jGjHGzRLMxptiDtOkbX7EVBgnafGSGDx1WGGVmJLClVh2LeeBMMo24IFv8wCyQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nan": { "version": "2.27.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", @@ -14235,9 +14254,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "5.1.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", - "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-6.0.1.tgz", + "integrity": "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==", "funding": [ { "type": "github", @@ -14249,15 +14268,9 @@ "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^18 || >=20" + "node": "^22 || ^24 || >=26" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14364,9 +14377,9 @@ } }, "node_modules/node-gyp/node_modules/undici": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", - "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "dev": true, "license": "MIT", "engines": { @@ -14396,6 +14409,22 @@ "dev": true, "license": "MIT" }, + "node_modules/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/node-pty/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.46", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", @@ -14466,6 +14495,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -14475,22 +14513,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14758,6 +14780,95 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -14766,9 +14877,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -14834,9 +14945,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -14854,7 +14965,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -14863,9 +14974,9 @@ } }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -14881,43 +14992,43 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/prebuild-install": { - "name": "@mmomtchev/prebuild-install", - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mmomtchev/prebuild-install/-/prebuild-install-1.0.2.tgz", - "integrity": "sha512-0Vje0eg5XQa8Ta1jtQiWqnT1kS/P7OAAvFJ4VZG5EXT2gJmpmPA5SrsDrh++BqNjIxYy8O6aF4wZjJc/k4JQ6w==", + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.3", - "expand-template": "^2.0.3", - "github-from-package": "^0.0.0", - "minimist": "^1.2.8", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.2", - "node-abi": "^3.63.0", - "pump": "^3.0.0", - "rc": "^1.2.8", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.6", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, "engines": { - "node": ">=18" + "node": ">=4" } }, - "node_modules/prebuild-install/node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "xtend": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, "node_modules/precond": { @@ -14939,9 +15050,9 @@ } }, "node_modules/prettier": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -15119,16 +15230,6 @@ "node": ">=10" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -15338,67 +15439,67 @@ } }, "node_modules/radix-ui": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.1.tgz", - "integrity": "sha512-QXDXJtB6sK83mLASONYUZCauatcWb+knFviFpN1EhtdbbmlsRmzCLrbZSKztnNiem2KOHIBbiDbauVB7SORXMw==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.7.tgz", + "integrity": "sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-accessible-icon": "1.1.11", - "@radix-ui/react-accordion": "1.2.15", - "@radix-ui/react-alert-dialog": "1.1.18", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-aspect-ratio": "1.1.11", - "@radix-ui/react-avatar": "1.2.1", - "@radix-ui/react-checkbox": "1.3.6", - "@radix-ui/react-collapsible": "1.1.15", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-context-menu": "2.3.2", - "@radix-ui/react-dialog": "1.1.18", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-dropdown-menu": "2.1.19", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-form": "0.1.11", - "@radix-ui/react-hover-card": "1.1.18", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-menu": "2.1.19", - "@radix-ui/react-menubar": "1.1.19", - "@radix-ui/react-navigation-menu": "1.2.17", - "@radix-ui/react-one-time-password-field": "0.1.11", - "@radix-ui/react-password-toggle-field": "0.1.6", - "@radix-ui/react-popover": "1.1.18", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-progress": "1.1.11", - "@radix-ui/react-radio-group": "1.4.2", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-scroll-area": "1.2.13", - "@radix-ui/react-select": "2.3.2", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-slider": "1.4.2", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-switch": "1.3.2", - "@radix-ui/react-tabs": "1.1.16", - "@radix-ui/react-toast": "1.2.18", - "@radix-ui/react-toggle": "1.1.13", - "@radix-ui/react-toggle-group": "1.1.14", - "@radix-ui/react-toolbar": "1.1.14", - "@radix-ui/react-tooltip": "1.2.11", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-escape-keydown": "1.1.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-accessible-icon": "1.1.15", + "@radix-ui/react-accordion": "1.2.20", + "@radix-ui/react-alert-dialog": "1.1.23", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-aspect-ratio": "1.1.15", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-checkbox": "1.3.11", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-context-menu": "2.3.7", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-form": "0.1.16", + "@radix-ui/react-hover-card": "1.1.23", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-menubar": "1.1.24", + "@radix-ui/react-navigation-menu": "1.2.22", + "@radix-ui/react-one-time-password-field": "0.1.16", + "@radix-ui/react-password-toggle-field": "0.1.11", + "@radix-ui/react-popover": "1.1.23", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-progress": "1.1.16", + "@radix-ui/react-radio-group": "1.4.7", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-scroll-area": "1.2.18", + "@radix-ui/react-select": "2.3.7", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slider": "1.4.7", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-switch": "1.3.7", + "@radix-ui/react-tabs": "1.1.21", + "@radix-ui/react-toast": "1.2.23", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-toggle-group": "1.1.19", + "@radix-ui/react-toolbar": "1.1.19", + "@radix-ui/react-tooltip": "1.2.16", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-escape-keydown": "1.1.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -15439,40 +15540,10 @@ "node": ">= 0.10" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "dev": true, "license": "MIT", "engines": { @@ -15494,16 +15565,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-h5-audio-player": { @@ -15525,9 +15596,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.79.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.79.0.tgz", - "integrity": "sha512-mhYp/MTmXvzYX6AJcJVko0rktoIhhmRnEouObj4wF5i/tCttgJvnp1+9wRkpITZjDTqpo4IOSJqu0dBlPlV/Lw==", + "version": "7.84.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.84.0.tgz", + "integrity": "sha512-+hWvQP6GLco56mDwrbU4XnHix8t1z90ltZsDIrREl+jnQFQxYLX8oAzqe/Xn8nHpmoXTY5M6oEXrAhbP1qevNQ==", "dev": true, "license": "MIT", "engines": { @@ -15542,20 +15613,20 @@ } }, "node_modules/react-i18next": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", - "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.11.tgz", + "integrity": "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==", "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "html-parse-stringify": "^3.0.1", + "html-parse-stringify": "^4.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", - "typescript": "^5 || ^6" + "typescript": "^5 || ^6 || ^7" }, "peerDependenciesMeta": { "react-dom": { @@ -15570,9 +15641,9 @@ } }, "node_modules/react-icons": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", - "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -15952,21 +16023,14 @@ "node": ">=8" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/retry": { @@ -15979,21 +16043,14 @@ "node": ">= 4" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -16003,21 +16060,20 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" } }, "node_modules/router": { @@ -16236,7 +16292,6 @@ "version": "0.35.3", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", @@ -16306,9 +16361,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "dev": true, "license": "MIT", "engines": { @@ -16410,51 +16465,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -16583,6 +16593,30 @@ "node": ">= 0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz", + "integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", @@ -16607,6 +16641,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -16648,17 +16692,6 @@ "node": ">=10.0.0" } }, - "node_modules/streamx": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.26.0.tgz", - "integrity": "sha512-VvNG1K72Po/xwJzxZFnZ++Tbrv4lwSptsbkFuzXCJAYZvCK5nnxsvXU6ajqkv7chyiI1Y0YXq2Jh8Iy8Y7NF/A==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -16842,9 +16875,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, @@ -16863,9 +16896,9 @@ } }, "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -16879,32 +16912,6 @@ "node": ">=18" } }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", - "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -16915,15 +16922,6 @@ "node": ">=18" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", @@ -16935,15 +16933,6 @@ "fs-extra": "^10.0.0" } }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -17016,22 +17005,22 @@ } }, "node_modules/tldts": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", - "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.2" + "tldts-core": "^7.4.10" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", - "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", "dev": true, "license": "MIT" }, @@ -17075,9 +17064,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -17142,6 +17131,12 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -17161,6 +17156,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tsyringe": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", @@ -17179,18 +17193,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", @@ -17272,16 +17274,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", - "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.61.1", - "@typescript-eslint/parser": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/utils": "8.61.1" + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17296,9 +17298,9 @@ } }, "node_modules/undici": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.7.0.tgz", - "integrity": "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -17652,16 +17654,16 @@ } }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -17678,7 +17680,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -17744,20 +17746,293 @@ "vite": ">=3.0.0" } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -17785,12 +18060,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -17834,16 +18109,6 @@ } } }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -17972,54 +18237,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -18027,9 +18244,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -18074,6 +18291,15 @@ "dev": true, "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 7216010..b4b7b25 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "termix", "private": true, - "version": "2.5.1", + "version": "2.7.0", "description": "Self-hosted SSH and remote desktop management.", "author": "Karmaa", "main": "electron/main.cjs", @@ -12,14 +12,14 @@ "scripts": { "format": "prettier --write .", "format:check": "prettier --check .", - "biome:check": "biome check biome.json package.json", - "biome:fix": "biome check --write biome.json package.json", - "postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-guacamole-lite.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-xterm-android-ime.cjs", + "postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-guacamole-lite.cjs && node scripts/patch-guacamole-common-js.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-xterm-android-ime.cjs", + "prepare": "husky || true", "prebuild": "node scripts/write-electron-build-info.cjs", - "lint": "eslint .", + "lint": "node scripts/generate-dialect-schema.cjs --check && eslint .", "lint:fix": "eslint --fix .", - "type-check": "tsc --noEmit", + "type-check": "tsc -b --force", "test": "vitest run", + "verify:dialect": "tsx scripts/verify-dialects.mjs", "test:watch": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", @@ -33,156 +33,167 @@ "preview": "vite preview", "electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"", "electron:patch-builder": "node scripts/patch-app-builder-lib.cjs", - "electron:rebuild": "electron-rebuild -f -w better-sqlite3 -w serialport", + "electron:rebuild": "electron-rebuild -f -o better-sqlite3,@serialport/bindings-cpp,node-pty", "build:win-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --dir", "build:win-installer": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --publish=never", "build:linux-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux --dir", "build:linux-appimage": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux AppImage", "build:linux-targz": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux tar.gz", "build:mac": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac --universal", - "build:mac-dev": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac dir --publish=never" + "build:mac-dev": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac dir --publish=never", + "schema:generate": "node scripts/generate-dialect-schema.cjs", + "schema:check": "node scripts/generate-dialect-schema.cjs --check", + "schema:migrations": "drizzle-kit generate --config=drizzle.config.sqlite.ts && drizzle-kit generate --config=drizzle.config.pg.ts && drizzle-kit generate --config=drizzle.config.mysql.ts" }, "dependencies": { + "@anthropic-ai/sdk": "^0.116.0", "@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/server": "^13.3.2", - "@tanstack/react-virtual": "^3.14.6", + "@tanstack/react-virtual": "^3.14.9", + "@types/compression": "^1.8.1", "@types/ldapjs": "^3.0.6", - "axios": "^1.18.1", + "axios": "^1.19.0", "bcryptjs": "^3.0.3", - "better-sqlite3": "^12.11.1", + "better-sqlite3": "^13.0.2", "body-parser": "^2.3.0", - "chalk": "^5.6.2", + "chalk": "^6.0.0", + "compression": "^1.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "express": "^5.2.1", "guacamole-lite": "^1.2.0", - "jose": "^6.2.2", - "js-yaml": "^5.2.1", + "jose": "^6.2.8", + "js-yaml": "^5.2.3", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", "ldapjs": "^3.0.7", - "motion": "^12.42.2", + "motion": "^12.43.0", "multer": "^2.2.0", - "nanoid": "^5.1.16", + "mysql2": "^3.23.2", + "nanoid": "^6.0.1", + "node-pty": "^1.1.0", + "pg": "^8.22.0", "qrcode": "^1.5.4", "serialport": "^13.0.0", + "sharp": "^0.35.3", "socks": "^2.8.7", "speakeasy": "^2.0.0", "ssh2": "^1.17.0", - "undici": "^8.7.0", - "ws": "^8.20.0" + "undici": "^8.10.0", + "ws": "^8.21.1" }, "devDependencies": { - "@biomejs/biome": "2.5.2", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.10.4", "@codemirror/search": "^6.7.1", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.5", - "@commitlint/cli": "^21.0.2", - "@commitlint/config-conventional": "^21.0.2", + "@codemirror/view": "^6.43.7", + "@commitlint/cli": "^21.2.1", + "@commitlint/config-conventional": "^21.2.0", "@deadendjs/swagger-jsdoc": "^8.1.2", "@electron/notarize": "^3.1.1", - "@electron/rebuild": "^4.0.4", + "@electron/rebuild": "^4.2.0", "@eslint/js": "^10.0.1", - "@fontsource-variable/jetbrains-mono": "^5.2.8", - "@fontsource/fira-code": "^5.2.7", - "@fontsource/jetbrains-mono": "^5.2.8", - "@fontsource/source-code-pro": "^5.2.7", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource/fira-code": "^5.3.0", + "@fontsource/jetbrains-mono": "^5.3.0", + "@fontsource/source-code-pro": "^5.3.0", "@monaco-editor/react": "^4.7.0", - "@radix-ui/react-accordion": "^1.2.15", - "@radix-ui/react-alert-dialog": "^1.1.18", - "@radix-ui/react-checkbox": "^1.3.6", - "@radix-ui/react-dialog": "^1.1.18", - "@radix-ui/react-dropdown-menu": "^2.1.19", - "@radix-ui/react-label": "^2.1.11", - "@radix-ui/react-popover": "^1.1.18", - "@radix-ui/react-progress": "^1.1.11", - "@radix-ui/react-scroll-area": "^1.2.13", - "@radix-ui/react-select": "^2.3.2", - "@radix-ui/react-separator": "^1.1.11", - "@radix-ui/react-slider": "^1.4.2", - "@radix-ui/react-slot": "^1.3.0", - "@radix-ui/react-switch": "^1.3.2", - "@radix-ui/react-tabs": "^1.1.16", - "@radix-ui/react-tooltip": "^1.2.11", - "@tailwindcss/vite": "^4.3.2", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-alert-dialog": "^1.1.23", + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-popover": "^1.1.23", + "@radix-ui/react-progress": "^1.1.16", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slider": "^1.4.7", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-switch": "^1.3.7", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tailwindcss/vite": "^4.3.3", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@types/better-sqlite3": "^7.6.13", + "@types/better-sqlite3": "^9.6.0", "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/guacamole-common-js": "^1.5.5", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/multer": "^2.1.0", - "@types/node": "^26.0.0", + "@types/multer": "^2.2.0", + "@types/node": "^26.1.2", + "@types/pg": "^8.20.3", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@types/speakeasy": "^2.0.10", "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", - "@uiw/codemirror-extensions-langs": "^4.25.9", - "@uiw/codemirror-theme-github": "^4.25.9", - "@uiw/react-codemirror": "^4.25.9", - "@vitejs/plugin-react": "^6.0.3", - "@vitest/coverage-v8": "^4.1.9", - "@vitest/ui": "^4.1.9", + "@uiw/codemirror-extensions-langs": "^4.25.11", + "@uiw/codemirror-theme-github": "^4.25.11", + "@uiw/react-codemirror": "^4.25.11", + "@vitejs/plugin-react": "^6.0.5", + "@vitest/coverage-v8": "^4.1.10", + "@vitest/ui": "^4.1.10", "@xterm/addon-clipboard": "^0.2.0", "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-search": "^0.16.0", "@xterm/addon-unicode11": "^0.9.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "concurrently": "^10.0.3", + "concurrently": "^10.0.4", "cytoscape": "^3.34.0", - "electron": "^43.0.0", + "drizzle-kit": "^0.31.10", + "electron": "^43.2.0", "electron-builder": "^26.15.3", - "eslint": "^10.5.0", + "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "eslint-plugin-unused-imports": "^4.4.1", - "globals": "^17.5.0", + "globals": "^17.8.0", "guacamole-common-js": "^1.5.0", "husky": "^9.1.7", - "i18next": "^26.3.4", + "i18next": "^26.3.6", "i18next-browser-languagedetector": "^8.2.1", - "jsdom": "^29.1.1", - "lint-staged": "^17.0.8", - "lucide-react": "^1.20.0", - "prettier": "3.8.4", - "radix-ui": "^1.6.1", - "react": "^19.2.7", + "jsdom": "^30.0.1", + "lint-staged": "^17.2.0", + "lucide-react": "^1.28.0", + "prettier": "3.9.6", + "radix-ui": "^1.6.7", + "react": "^19.2.8", "react-cytoscapejs": "^2.0.0", - "react-dom": "^19.2.7", + "react-dom": "^19.2.8", "react-h5-audio-player": "^3.10.2", - "react-hook-form": "^7.79.0", - "react-i18next": "^17.0.4", - "react-icons": "^5.6.0", + "react-hook-form": "^7.84.0", + "react-i18next": "^17.0.11", + "react-icons": "^5.7.0", "react-markdown": "^10.1.0", "react-pdf": "^10.4.1", "react-photo-view": "^1.2.7", "react-syntax-highlighter": "^16.1.1", "react-xtermjs": "^1.0.10", "remark-gfm": "^4.0.1", - "sharp": "^0.35.3", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", "tw-animate-css": "^1.4.0", "typescript": "~6.0.3", - "typescript-eslint": "^8.61.1", - "vite": "^8.0.16", + "typescript-eslint": "^8.66.0", + "vite": "^8.2.0", "vite-plugin-svgr": "^5.2.0", - "vitest": "^4.1.9" + "vitest": "^4.1.10" }, "lint-staged": { "*.{ts,tsx}": [ @@ -202,6 +213,18 @@ "dompurify": "^3.4.1", "eslint-visitor-keys": "^4.2.1", "prebuild-install": "npm:@mmomtchev/prebuild-install@1.0.2", - "rimraf": "file:vendor/rimraf-compat" + "rimraf": "file:vendor/rimraf-compat", + "brace-expansion@1": "^1.1.18", + "brace-expansion@2": "^2.1.4", + "brace-expansion@5": "^5.0.9", + "esbuild": "^0.28.1", + "fast-uri@3": "^3.1.5", + "ip-address": "^10.5.0", + "js-yaml@4": "^4.3.1", + "nanoid@3": "^3.3.18", + "postcss": "^8.5.26", + "tar": "^7.5.22", + "undici@6": "^6.28.0", + "undici@7": "^7.29.0" } } diff --git a/scripts/electron-app-quit.test.ts b/scripts/electron-app-quit.test.ts new file mode 100644 index 0000000..529a30e --- /dev/null +++ b/scripts/electron-app-quit.test.ts @@ -0,0 +1,28 @@ +import { createRequire } from "node:module"; +import { describe, expect, it, vi } from "vitest"; + +const require = createRequire(import.meta.url); +const { quitApp } = require("../electron/app-quit.cjs") as { + quitApp: ( + app: { quit: () => void }, + window: { destroy: () => void } | null, + ) => void; +}; + +describe("Electron app quit", () => { + it("destroys the window before quitting so renderer unload guards cannot cancel it", () => { + const calls: string[] = []; + quitApp( + { quit: vi.fn(() => calls.push("quit")) }, + { destroy: vi.fn(() => calls.push("destroy")) }, + ); + + expect(calls).toEqual(["destroy", "quit"]); + }); + + it("still quits after the window has already gone", () => { + const quit = vi.fn(); + quitApp({ quit }, null); + expect(quit).toHaveBeenCalledOnce(); + }); +}); diff --git a/scripts/electron-keyboard-shortcuts.test.ts b/scripts/electron-keyboard-shortcuts.test.ts new file mode 100644 index 0000000..39670d7 --- /dev/null +++ b/scripts/electron-keyboard-shortcuts.test.ts @@ -0,0 +1,33 @@ +import { createRequire } from "node:module"; +import { describe, expect, it } from "vitest"; + +const require = createRequire(import.meta.url); +const { isCloseActiveTabInput } = + require("../electron/keyboard-shortcuts.cjs") as { + isCloseActiveTabInput: (input: { + type: string; + key: string; + control?: boolean; + alt?: boolean; + shift?: boolean; + meta?: boolean; + }) => boolean; + }; + +describe("Electron keyboard shortcuts", () => { + it("recognizes Ctrl+W without extra modifiers", () => { + expect( + isCloseActiveTabInput({ type: "keyDown", key: "w", control: true }), + ).toBe(true); + }); + + it.each([ + { type: "keyUp", key: "w", control: true }, + { type: "keyDown", key: "w", control: true, alt: true }, + { type: "keyDown", key: "w", control: true, shift: true }, + { type: "keyDown", key: "w", meta: true }, + { type: "keyDown", key: "q", control: true }, + ])("does not consume other input: %o", (input) => { + expect(isCloseActiveTabInput(input)).toBe(false); + }); +}); diff --git a/scripts/electron-linux-password-store.test.ts b/scripts/electron-linux-password-store.test.ts new file mode 100644 index 0000000..57501a6 --- /dev/null +++ b/scripts/electron-linux-password-store.test.ts @@ -0,0 +1,65 @@ +import { createRequire } from "node:module"; +import { describe, expect, it, vi } from "vitest"; + +const require = createRequire(import.meta.url); +const { selectLinuxPasswordStore } = + require("../electron/linux-password-store.cjs") as { + selectLinuxPasswordStore: ( + commandLine: { + hasSwitch: (name: string) => boolean; + appendSwitch: (name: string, value: string) => void; + }, + env: NodeJS.ProcessEnv, + ) => string | null; + }; + +function commandLine(existing: string[] = []) { + const appended: Array<[string, string]> = []; + return { + appended, + hasSwitch: (name: string) => existing.includes(name), + appendSwitch: vi.fn((name: string, value: string) => + appended.push([name, value]), + ), + }; +} + +describe("Linux password store selection", () => { + it("names libsecret on desktops Chromium has no mapping for", () => { + // Without this the backend resolves to "basic_text", and safeStorage + // reports encryption as unavailable even though a keyring is running. + for (const desktop of ["Hyprland", "sway", "niri", "river", "wayfire"]) { + const cmd = commandLine(); + expect( + selectLinuxPasswordStore(cmd, { XDG_CURRENT_DESKTOP: desktop }), + ).toBe("gnome-libsecret"); + expect(cmd.appended).toEqual([["password-store", "gnome-libsecret"]]); + } + }); + + it("leaves KWallet desktops to auto-detection", () => { + for (const env of [ + { XDG_CURRENT_DESKTOP: "KDE" }, + { XDG_CURRENT_DESKTOP: "KDE", DESKTOP_SESSION: "plasma" }, + { DESKTOP_SESSION: "/usr/share/xsessions/plasma" }, + { XDG_CURRENT_DESKTOP: "LXQt" }, + ]) { + const cmd = commandLine(); + expect(selectLinuxPasswordStore(cmd, env)).toBeNull(); + expect(cmd.appendSwitch).not.toHaveBeenCalled(); + } + }); + + it("never overrides a password store the user asked for", () => { + const cmd = commandLine(["password-store"]); + expect( + selectLinuxPasswordStore(cmd, { XDG_CURRENT_DESKTOP: "Hyprland" }), + ).toBeNull(); + expect(cmd.appendSwitch).not.toHaveBeenCalled(); + }); + + it("selects libsecret when the desktop is unset, matching a bare session", () => { + const cmd = commandLine(); + expect(selectLinuxPasswordStore(cmd, {})).toBe("gnome-libsecret"); + }); +}); diff --git a/scripts/generate-appstore-notes.cjs b/scripts/generate-appstore-notes.cjs new file mode 100644 index 0000000..778297c --- /dev/null +++ b/scripts/generate-appstore-notes.cjs @@ -0,0 +1,135 @@ +const fs = require("fs"); +const path = require("path"); + +// Apple caps the "What's New in This Version" field at 4000 characters. +const MAX_LENGTH = 4000; + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (!arg.startsWith("--")) continue; + const key = arg.slice(2); + const next = argv[i + 1]; + if (next === undefined || next.startsWith("--")) { + args[key] = true; + } else { + args[key] = next; + i++; + } + } + return args; +} + +function fail(message) { + console.error(`generate-appstore-notes: ${message}`); + process.exit(1); +} + +function extractSection(notes, name, { required = true } = {}) { + const pattern = new RegExp( + `([\\s\\S]*?)`, + ); + const match = notes.match(pattern); + if (!match) { + if (required) fail(`missing section in release notes`); + return ""; + } + return match[1].trim(); +} + +// Strip markdown that reads badly as plain text in App Store Connect. +function toPlainText(markdown) { + return markdown + .split("\n") + .map((line) => { + let text = line.replace(/\r$/, ""); + const indent = text.match(/^\s*/)[0].length; + text = text.trim(); + text = text.replace(/^[-*]\s+/, indent >= 2 ? " - " : "- "); + text = text.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1"); + text = text.replace(/`([^`]+)`/g, "$1"); + text = text.replace(/\*\*([^*]+)\*\*/g, "$1"); + text = text.replace(/^#+\s*/, ""); + return text; + }) + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +// Drop whole trailing lines until the text fits, so we never cut a bullet +// in half or leave a dangling section header. +function truncate(text, limit) { + if (text.length <= limit) return text; + + const lines = text.split("\n"); + while (lines.length > 0 && lines.join("\n").length > limit) { + lines.pop(); + } + while (lines.length > 0 && !lines[lines.length - 1].trim()) { + lines.pop(); + } + // A section header left with no bullets under it is noise. + while (lines.length > 0 && /^[A-Za-z ]+:$/.test(lines[lines.length - 1])) { + lines.pop(); + while (lines.length > 0 && !lines[lines.length - 1].trim()) lines.pop(); + } + return lines.join("\n").trim(); +} + +function buildNotes(notesFile) { + const summary = extractSection(notesFile, "SUMMARY"); + const updateLog = extractSection(notesFile, "UPDATE_LOG", { + required: false, + }); + const bugFixes = extractSection(notesFile, "BUG_FIXES", { required: false }); + + const parts = [toPlainText(summary)]; + if (updateLog) parts.push("", "Update Log:", toPlainText(updateLog)); + if (bugFixes) parts.push("", "Bug Fixes:", toPlainText(bugFixes)); + + const body = parts + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); + if (!body) fail("release notes produced no text"); + return truncate(body, MAX_LENGTH); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const notesPath = args.notes || "RELEASE_NOTES.md"; + const outDir = args["out-dir"]; + const locales = String(args.locales || "en-US") + .split(",") + .map((locale) => locale.trim()) + .filter(Boolean); + + if (!outDir || outDir === true) fail("--out-dir is required"); + if (locales.length === 0) fail("--locales resolved to no locales"); + + const resolvedNotes = path.resolve(notesPath); + if (!fs.existsSync(resolvedNotes)) { + fail(`release notes file not found: ${resolvedNotes}`); + } + + const notes = buildNotes(fs.readFileSync(resolvedNotes, "utf8")); + + for (const locale of locales) { + const localeDir = path.join(path.resolve(outDir), locale); + fs.mkdirSync(localeDir, { recursive: true }); + fs.writeFileSync( + path.join(localeDir, "release_notes.txt"), + notes + "\n", + "utf8", + ); + console.log(`Wrote ${locale}/release_notes.txt (${notes.length} chars)`); + } +} + +if (require.main === module) { + main(); +} + +module.exports = { buildNotes, toPlainText, truncate, MAX_LENGTH }; diff --git a/scripts/generate-appstore-notes.test.ts b/scripts/generate-appstore-notes.test.ts new file mode 100644 index 0000000..7e5484f --- /dev/null +++ b/scripts/generate-appstore-notes.test.ts @@ -0,0 +1,126 @@ +import { describe, it, expect } from "vitest"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { + buildNotes, + toPlainText, + truncate, + MAX_LENGTH, +} = require("./generate-appstore-notes.cjs"); + +function notesFile(sections: Record) { + return Object.entries(sections) + .map(([name, body]) => `\n${body}\n`) + .join("\n\n"); +} + +describe("toPlainText", () => { + it("strips markdown links down to their label", () => { + expect(toPlainText("- See [the docs](https://example.com)")).toBe( + "- See the docs", + ); + }); + + it("strips backticks and bold markers", () => { + expect(toPlainText("- Fixed `npm run build` and **crashes**")).toBe( + "- Fixed npm run build and crashes", + ); + }); + + it("keeps nested bullets indented", () => { + expect(toPlainText("- Top\n - Nested")).toBe("- Top\n - Nested"); + }); + + it("normalizes asterisk bullets to dashes", () => { + expect(toPlainText("* One\n* Two")).toBe("- One\n- Two"); + }); + + it("collapses runs of blank lines", () => { + expect(toPlainText("- One\n\n\n\n- Two")).toBe("- One\n\n- Two"); + }); +}); + +describe("truncate", () => { + it("leaves text under the limit untouched", () => { + expect(truncate("- One\n- Two", 100)).toBe("- One\n- Two"); + }); + + it("drops whole trailing lines rather than splitting one", () => { + const result = truncate("- One\n- Two\n- Three", 12); + expect(result).toBe("- One\n- Two"); + }); + + it("drops a section header left with no bullets under it", () => { + const result = truncate("- One\n\nBug Fixes:\n- Two", 18); + expect(result).toBe("- One"); + }); +}); + +describe("buildNotes", () => { + it("includes the summary, update log, and bug fixes", () => { + const notes = buildNotes( + notesFile({ + SUMMARY: "A big release.", + UPDATE_LOG: "- Added a thing", + BUG_FIXES: "- Fixed a thing", + }), + ); + + expect(notes).toContain("A big release."); + expect(notes).toContain("Update Log:"); + expect(notes).toContain("- Added a thing"); + expect(notes).toContain("Bug Fixes:"); + expect(notes).toContain("- Fixed a thing"); + }); + + it("omits optional sections that are absent", () => { + const notes = buildNotes(notesFile({ SUMMARY: "Small release." })); + + expect(notes).toBe("Small release."); + expect(notes).not.toContain("Update Log:"); + expect(notes).not.toContain("Bug Fixes:"); + }); + + it("stays within the App Store character limit", () => { + const notes = buildNotes( + notesFile({ + SUMMARY: "Big release.", + UPDATE_LOG: Array.from( + { length: 400 }, + (_, i) => `- Added feature number ${i}`, + ).join("\n"), + BUG_FIXES: Array.from( + { length: 400 }, + (_, i) => `- Fixed bug number ${i}`, + ).join("\n"), + }), + ); + + expect(notes.length).toBeLessThanOrEqual(MAX_LENGTH); + expect(notes.length).toBeGreaterThan(0); + }); + + it("never ends mid-bullet when truncating", () => { + const notes = buildNotes( + notesFile({ + SUMMARY: "Big release.", + UPDATE_LOG: Array.from( + { length: 400 }, + (_, i) => `- Added feature number ${i}`, + ).join("\n"), + }), + ); + + const lines = notes.split("\n"); + expect(lines[lines.length - 1]).toMatch(/^- Added feature number \d+$/); + }); + + it("produces non-empty notes for the real release notes file", () => { + const fs = require("fs"); + const notes = buildNotes(fs.readFileSync("RELEASE_NOTES.md", "utf8")); + + expect(notes.length).toBeGreaterThan(0); + expect(notes.length).toBeLessThanOrEqual(MAX_LENGTH); + }); +}); diff --git a/scripts/generate-dialect-schema.cjs b/scripts/generate-dialect-schema.cjs new file mode 100644 index 0000000..171279d --- /dev/null +++ b/scripts/generate-dialect-schema.cjs @@ -0,0 +1,236 @@ +/** + * Generates the Postgres and MySQL schema modules from the SQLite one. + * + * ## These files produce DDL. They are not used at runtime. + * + * drizzle-kit reads them to emit the migrations in drizzle/postgres and + * drizzle/mysql. Nothing imports them to run a query. + * + * That is not an oversight. The query builder needs two things from a table + * object โ€” the identifiers to interpolate, and the encoders that turn JS values + * into driver values โ€” and the sqlite definitions supply both correctly for + * every engine, which is why all 44 repositories import schema.ts directly: + * + * - text and integer encode as themselves everywhere + * - integer({ mode: "boolean" }) writes 1/0, which Postgres and MySQL both + * accept for a boolean column, and reads back through `Number(v) === 1`, + * which is true for JS `true` as well as for 1 + * - real is a plain number on all three + * + * What genuinely differs between the dialects is DDL โ€” column types, key + * lengths, autoincrement syntax โ€” and DDL is exactly what these files exist to + * generate. See scripts/verify-dialects.mjs, which asserts the round-trips + * above against real servers rather than trusting this comment. + * + * The schema is declared once, in sqlite-core, and the other two dialects are + * derived. Hand-maintaining three copies of 52 tables would mean a renamed + * table has to land in three places consistently or a foreign key silently + * points at the wrong one โ€” and the schema is regular enough that the mapping + * is mechanical. + * + * What varies between dialects is small and closed: + * - booleans are integers on sqlite, native elsewhere + * - autoincrement keys are `integer primary key autoincrement`, `serial`, + * and `int auto_increment` + * - MySQL cannot index unbounded TEXT, so any column that is a primary key, + * is unique, or participates in a foreign key must be varchar + * - MySQL rejects a bare DEFAULT CURRENT_TIMESTAMP on a text column, so it is + * written as a parenthesised expression default + * + * Usage: node scripts/generate-dialect-schema.cjs [--check] + * --check verifies the committed files match what would be generated, + * for CI to catch a schema edit that forgot to regenerate. + */ + +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.join(__dirname, ".."); +const SOURCE = path.join(ROOT, "src/backend/database/db/schema.ts"); +const TARGETS = { + postgres: path.join(ROOT, "src/backend/database/db/schema.pg.ts"), + mysql: path.join(ROOT, "src/backend/database/db/schema.mysql.ts"), +}; + +const KEY_LENGTH = 255; + +/** + * Columns that must be varchar rather than text on MySQL. A column qualifies if + * it is a primary key, is unique, or is either end of a foreign key. + */ +function collectKeyColumns(source) { + const keyed = new Set(); + + // `name: text("col")....primaryKey()` / `.unique()` / `.references(...)` + const declaration = + /(\w+):\s*text\("([a-z0-9_]+)"\)((?:\s*\.\w+\([^)]*\))*)/g; + let match; + while ((match = declaration.exec(source)) !== null) { + const [, prop, column, modifiers] = match; + if (/\.(primaryKey|unique|references)\(/.test(modifiers)) { + keyed.add(column); + } + void prop; + } + + // Multi-line form: the modifiers land on following lines. + const multiline = + /(\w+):\s*text\("([a-z0-9_]+)"\)\s*\n(\s*\.\w+\([\s\S]*?\),)/g; + while ((match = multiline.exec(source)) !== null) { + if (/\.(primaryKey|unique|references)\(/.test(match[3])) { + keyed.add(match[2]); + } + } + + // A referenced column implies the referencing side too; both must match. + const reference = /\.references\(\(\)\s*=>\s*\w+\.(\w+)/g; + while ((match = reference.exec(source)) !== null) { + keyed.add(camelToSnake(match[1])); + } + + // Table-level indexes: `(table) => [uniqueIndex("x").on(table.a, table.b)]`, + // and the same for the plain `index("x")` used by the performance indexes. + // These were invisible here at first, and MySQL rejected the migration with + // "BLOB/TEXT column used in key specification without a key length" โ€” but + // only on MySQL 8; MariaDB took it. + const tableIndex = /\b(?:unique)?[iI]ndex\("[a-z0-9_]+"\)\.on\(([^)]*)\)/g; + while ((match = tableIndex.exec(source)) !== null) { + for (const column of match[1].split(",")) { + const name = column.trim().replace(/^\w+\./, ""); + if (name) keyed.add(camelToSnake(name)); + } + } + + return keyed; +} + +function camelToSnake(value) { + return value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`); +} + +function transform(source, dialect) { + const keyed = collectKeyColumns(source); + const isPg = dialect === "postgres"; + let out = source; + + // Autoincrement primary keys, before the plain integer rule below. + out = out.replace( + /integer\("([a-z0-9_]+)"\)\.primaryKey\(\{\s*autoIncrement:\s*true\s*\}\)/g, + (_, col) => + isPg + ? `serial("${col}").primaryKey()` + : `int("${col}").autoincrement().primaryKey()`, + ); + + // Integer-backed booleans become native ones. Prettier wraps the longer + // declarations across lines, so this has to span newlines too. + out = out.replace( + /integer\(\s*"([a-z0-9_]+)",\s*\{\s*mode:\s*"boolean",?\s*\},?\s*\)/g, + (_, col) => `boolean("${col}")`, + ); + + // Remaining integers. + if (!isPg) { + out = out.replace( + /\binteger\("([a-z0-9_]+)"\)/g, + (_, col) => `int("${col}")`, + ); + + // Timestamps are stored as text (see sql-timestamp.ts). MySQL only accepts + // DEFAULT CURRENT_TIMESTAMP on a DATETIME or TIMESTAMP column โ€” on a TEXT + // one it is ER_INVALID_DEFAULT, "Invalid default value". Since 8.0.13 an + // expression default works on any type, and an expression is written + // parenthesised. MariaDB accepts the bare form, which is why this only + // surfaces against real MySQL. + out = out.replace(/sql`CURRENT_TIMESTAMP`/g, "sql`(CURRENT_TIMESTAMP)`"); + } + + // Floating point. + out = out.replace(/\breal\("([a-z0-9_]+)"\)/g, (_, col) => + isPg ? `doublePrecision("${col}")` : `double("${col}")`, + ); + + // Key-bearing strings must be indexable. + out = out.replace(/\btext\("([a-z0-9_]+)"\)/g, (whole, col) => + keyed.has(col) ? `varchar("${col}", { length: ${KEY_LENGTH} })` : whole, + ); + + // text("x", { length: n }) is sqlite-only sugar; drop the length. + out = out.replace( + /\btext\("([a-z0-9_]+)",\s*\{\s*length:\s*\d+\s*\}\)/g, + (_, col) => `text("${col}")`, + ); + + out = out.replace(/\bsqliteTable\(/g, isPg ? "pgTable(" : "mysqlTable("); + + // Self-referencing FK callbacks are typed against the source dialect's + // "any column" helper so TS can resolve the circular table reference. + out = out.replace( + /\bAnySQLiteColumn\b/g, + isPg ? "AnyPgColumn" : "AnyMySqlColumn", + ); + + const imports = isPg + ? `import {\n pgTable,\n text,\n varchar,\n integer,\n serial,\n boolean,\n doublePrecision,\n index,\n uniqueIndex,\n type AnyPgColumn,\n} from "drizzle-orm/pg-core";` + : `import {\n mysqlTable,\n text,\n varchar,\n int,\n boolean,\n double,\n index,\n uniqueIndex,\n type AnyMySqlColumn,\n} from "drizzle-orm/mysql-core";`; + + out = out.replace( + /import\s*\{[^}]*\}\s*from\s*"drizzle-orm\/sqlite-core";/, + imports, + ); + + return `${header(dialect)}\n${out}`; +} + +function header(dialect) { + return `// GENERATED FILE โ€” do not edit. +// +// Produced from schema.ts by scripts/generate-dialect-schema.cjs. +// Edit the sqlite schema and re-run \`node scripts/generate-dialect-schema.cjs\`. +// Target dialect: ${dialect}. +// +// DDL source for drizzle-kit. NOT imported to run queries โ€” repositories use +// schema.ts on every dialect. See the generator header for why that is correct. +`; +} + +function main() { + const check = process.argv.includes("--check"); + const source = fs.readFileSync(SOURCE, "utf8"); + let drift = false; + + for (const [dialect, target] of Object.entries(TARGETS)) { + const generated = transform(source, dialect); + + if (check) { + const current = fs.existsSync(target) + ? fs.readFileSync(target, "utf8") + : ""; + if (current !== generated) { + console.error( + `[generate-dialect-schema] ${path.relative(ROOT, target)} is out of date`, + ); + drift = true; + } + continue; + } + + fs.writeFileSync(target, generated); + console.log( + `[generate-dialect-schema] wrote ${path.relative(ROOT, target)}`, + ); + } + + if (drift) { + console.error( + "[generate-dialect-schema] run `node scripts/generate-dialect-schema.cjs` and commit the result", + ); + process.exit(1); + } +} + +module.exports = { transform, collectKeyColumns }; + +if (require.main === module) { + main(); +} diff --git a/scripts/generate-dialect-schema.test.ts b/scripts/generate-dialect-schema.test.ts new file mode 100644 index 0000000..dad931f --- /dev/null +++ b/scripts/generate-dialect-schema.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from "vitest"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { transform, collectKeyColumns } = + require("./generate-dialect-schema.cjs") as { + transform: (source: string, dialect: "postgres" | "mysql") => string; + collectKeyColumns: (source: string) => Set; + }; + +const SOURCE = `import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; + +export const users = sqliteTable("users", { + id: text("id").primaryKey(), + username: text("username").notNull(), + isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false), + wrapped: integer("wrapped", { + mode: "boolean", + }) + .notNull() + .default(true), + score: real("score"), + ssoProviderId: integer("sso_provider_id"), +}); + +export const folders = sqliteTable("folders", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + syncId: text("sync_id").unique(), + cert: text("cert", { length: 8192 }), +}); +`; + +describe("collectKeyColumns", () => { + it("finds columns that must be indexable", () => { + const keyed = collectKeyColumns(SOURCE); + + // primary key, unique, and both ends of the foreign key + expect(keyed.has("id")).toBe(true); + expect(keyed.has("sync_id")).toBe(true); + expect(keyed.has("user_id")).toBe(true); + }); + + it("leaves ordinary strings alone", () => { + const keyed = collectKeyColumns(SOURCE); + + expect(keyed.has("username")).toBe(false); + expect(keyed.has("name")).toBe(false); + expect(keyed.has("cert")).toBe(false); + }); +}); + +describe("postgres output", () => { + const out = transform(SOURCE, "postgres"); + + it("is marked generated", () => { + expect(out.startsWith("// GENERATED FILE")).toBe(true); + }); + + it("uses pg-core", () => { + expect(out).toContain('from "drizzle-orm/pg-core"'); + expect(out).not.toContain("sqlite-core"); + expect(out).toContain("pgTable("); + expect(out).not.toContain("sqliteTable("); + }); + + it("maps autoincrement keys to serial", () => { + expect(out).toContain('serial("id").primaryKey()'); + expect(out).not.toContain("autoIncrement"); + }); + + it("maps integer-backed booleans, including the wrapped form", () => { + expect(out).toContain('boolean("is_admin")'); + // Prettier splits longer declarations across lines; both must convert. + expect(out).toContain('boolean("wrapped")'); + expect(out).not.toMatch(/mode:\s*"boolean"/); + }); + + it("keeps plain integers and maps real", () => { + expect(out).toContain('integer("sso_provider_id")'); + expect(out).toContain('doublePrecision("score")'); + }); + + it("makes key columns varchar and leaves the rest text", () => { + expect(out).toContain('varchar("id", { length: 255 })'); + expect(out).toContain('varchar("user_id", { length: 255 })'); + expect(out).toContain('varchar("sync_id", { length: 255 })'); + expect(out).toContain('text("username")'); + expect(out).toContain('text("name")'); + }); + + it("drops the sqlite-only text length", () => { + expect(out).toContain('text("cert")'); + expect(out).not.toContain("length: 8192"); + }); +}); + +describe("mysql output", () => { + const out = transform(SOURCE, "mysql"); + + it("uses mysql-core", () => { + expect(out).toContain('from "drizzle-orm/mysql-core"'); + expect(out).toContain("mysqlTable("); + }); + + it("maps autoincrement keys to int auto_increment", () => { + expect(out).toContain('int("id").autoincrement().primaryKey()'); + }); + + it("renames integer to int", () => { + expect(out).toContain('int("sso_provider_id")'); + expect(out).not.toMatch(/\binteger\(/); + }); + + it("maps real to double", () => { + expect(out).toContain('double("score")'); + }); + + it("makes key columns varchar โ€” MySQL cannot index unbounded TEXT", () => { + expect(out).toContain('varchar("user_id", { length: 255 })'); + expect(out).toContain('text("name")'); + }); +}); + +describe("determinism", () => { + it("produces identical output for identical input", () => { + expect(transform(SOURCE, "postgres")).toBe(transform(SOURCE, "postgres")); + expect(transform(SOURCE, "mysql")).toBe(transform(SOURCE, "mysql")); + }); + + it("keeps foreign key behaviour verbatim", () => { + for (const dialect of ["postgres", "mysql"] as const) { + expect(transform(SOURCE, dialect)).toContain('onDelete: "cascade"'); + } + }); +}); diff --git a/scripts/patch-guacamole-common-js.cjs b/scripts/patch-guacamole-common-js.cjs new file mode 100644 index 0000000..039071d --- /dev/null +++ b/scripts/patch-guacamole-common-js.cjs @@ -0,0 +1,68 @@ +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.join( + __dirname, + "..", + "node_modules", + "guacamole-common-js", +); + +const bundlePaths = [ + path.join(packageRoot, "dist", "esm", "guacamole-common.js"), + path.join(packageRoot, "dist", "cjs", "guacamole-common.js"), +]; + +const oldFlushBlock = + " if (window.requestAnimationFrame && document.hasFocus())\n" + + " asyncFlush();\n" + + " else\n" + + " syncFlush();"; + +const newFlushBlock = + " // Electron can throttle or skip requestAnimationFrame() for inactive\n" + + " // windows/tabs even while guacd is still sending display frames. Flush\n" + + " // synchronously so Guacamole connections do not stall while waiting for\n" + + " // a frame callback that may never run.\n" + + " syncFlush();"; + +let patched = false; +let foundBundle = false; + +for (const bundlePath of bundlePaths) { + if (!fs.existsSync(bundlePath)) { + console.log( + `[patch-guacamole-common-js] ${bundlePath} not found, skipping`, + ); + continue; + } + + foundBundle = true; + let content = fs.readFileSync(bundlePath, "utf8"); + if (content.includes(newFlushBlock)) continue; + + if (!content.includes(oldFlushBlock)) { + console.log( + `[patch-guacamole-common-js] Flush target not found in ${bundlePath}, skipping`, + ); + continue; + } + + content = content.replace(oldFlushBlock, newFlushBlock); + fs.writeFileSync(bundlePath, content); + patched = true; +} + +if (!foundBundle) { + console.log("[patch-guacamole-common-js] File not found, skipping"); + process.exit(0); +} + +if (!patched) { + console.log("[patch-guacamole-common-js] Already patched"); + process.exit(0); +} + +console.log( + "[patch-guacamole-common-js] Patched display flush to avoid Electron requestAnimationFrame stalls", +); diff --git a/scripts/patch-guacamole-lite.cjs b/scripts/patch-guacamole-lite.cjs index 67cdca6..c3d37bc 100644 --- a/scripts/patch-guacamole-lite.cjs +++ b/scripts/patch-guacamole-lite.cjs @@ -17,14 +17,41 @@ const cryptPath = path.join( "lib", "Crypt.js", ); +const clientConnectionPath = path.join( + __dirname, + "..", + "node_modules", + "guacamole-lite", + "lib", + "ClientConnection.js", +); -if (!fs.existsSync(guacdClientPath) || !fs.existsSync(cryptPath)) { +if ( + !fs.existsSync(guacdClientPath) || + !fs.existsSync(cryptPath) || + !fs.existsSync(clientConnectionPath) +) { console.log("[patch-guacamole-lite] File not found, skipping"); process.exit(0); } +// Every patch below is required for correctness: protocol negotiation, the +// guacd 1.6.0 name handshake, dynamic argument answering, UTF-8 tokens and +// read-only joins. If an upstream release moves an anchor string, silently +// skipping would ship a Termix that looks fine and then drops VNC/RDP sessions +// at runtime, so a missing anchor has to stop the install instead. +function missingAnchor(patch) { + console.error( + `[patch-guacamole-lite] ${patch} anchor not found in guacamole-lite. ` + + "The upstream file has changed and this patch no longer applies โ€” " + + "update scripts/patch-guacamole-lite.cjs to match the new source.", + ); + process.exit(1); +} + let guacdClientContent = fs.readFileSync(guacdClientPath, "utf8"); let cryptContent = fs.readFileSync(cryptPath, "utf8"); +let clientConnectionContent = fs.readFileSync(clientConnectionPath, "utf8"); // Patch 1: protocol version negotiation. // guacamole-lite originally only accepted 1.0.0/1.1.0. Support the protocol @@ -56,20 +83,26 @@ const newVersionBlock = const oldTimezone = "if (protocolVersion === '1_1_0') {"; const newTimezone = "if (protocolVersion !== '1_0_0') {"; -// Patch 3: send the `name` handshake instruction for protocol >= 1.3.0. -// The Guacamole protocol added the `name` instruction in 1.3.0 (an optional -// human-readable identifier for the joining user). guacd 1.6.0 began requiring -// it during the VNC handshake even when negotiating older protocol versions, -// causing connections to silently drop right after "User joined". See +// Patch 3: send the `name` handshake instruction for all protocol versions >= 1.1.0. +// The Guacamole protocol added `name` in 1.3.0, but guacd 1.6.0 began requiring it +// during the VNC handshake even when negotiating VERSION_1_1_0, causing connections to +// silently drop right after "User joined". Sending it for all non-1.0.0 sessions is +// harmless (guacd ignores unknown handshake instructions for older versions). See // Termix-SSH/Support#567 and #734. const oldConnect = " this.sendInstruction(['connect'].concat(connectArgs));"; -const newConnect = +const oldNameConnect = " if (protocolVersion === '1_3_0' || protocolVersion === '1_5_0') {\n" + " this.sendInstruction(['name', this.connectionSettings.name || 'guacamole-lite']);\n" + " }\n" + "\n" + " this.sendInstruction(['connect'].concat(connectArgs));"; +const newConnect = + " if (protocolVersion !== '1_0_0') {\n" + + " this.sendInstruction(['name', this.connectionSettings.name || 'guacamole-lite']);\n" + + " }\n" + + "\n" + + " this.sendInstruction(['connect'].concat(connectArgs));"; // Patch 4: answer guacd's dynamic argument requests locally. // macOS Screen Sharing can request VNC username/password through the @@ -138,40 +171,33 @@ if (!guacdClientContent.includes("} else if (/^1_\\d+_0$/.test(version)) {")) { newVersionBlock, ); } else { - console.log( - "[patch-guacamole-lite] Version check target not found, skipping", - ); - process.exit(0); + missingAnchor("Version check"); } patched = true; } if (!guacdClientContent.includes(newTimezone)) { if (!guacdClientContent.includes(oldTimezone)) { - console.log("[patch-guacamole-lite] Timezone target not found, skipping"); - process.exit(0); + missingAnchor("Timezone"); } guacdClientContent = guacdClientContent.replace(oldTimezone, newTimezone); patched = true; } if (!guacdClientContent.includes(newConnect)) { - if (!guacdClientContent.includes(oldConnect)) { - console.log( - "[patch-guacamole-lite] Connect target not found, skipping name patch", - ); - process.exit(0); + if (guacdClientContent.includes(oldNameConnect)) { + guacdClientContent = guacdClientContent.replace(oldNameConnect, newConnect); + } else if (guacdClientContent.includes(oldConnect)) { + guacdClientContent = guacdClientContent.replace(oldConnect, newConnect); + } else { + missingAnchor("Connect"); } - guacdClientContent = guacdClientContent.replace(oldConnect, newConnect); patched = true; } if (!guacdClientContent.includes("this.nextArgumentStreamIndex = 0;")) { if (!guacdClientContent.includes(oldSendBuffer)) { - console.log( - "[patch-guacamole-lite] Argument stream index target not found, skipping", - ); - process.exit(0); + missingAnchor("Argument stream index"); } guacdClientContent = guacdClientContent.replace(oldSendBuffer, newSendBuffer); patched = true; @@ -179,10 +205,7 @@ if (!guacdClientContent.includes("this.nextArgumentStreamIndex = 0;")) { if (!guacdClientContent.includes("sendRequiredArguments(params) {")) { if (!guacdClientContent.includes(oldSendInstructionBlock)) { - console.log( - "[patch-guacamole-lite] Required argument helper target not found, skipping", - ); - process.exit(0); + missingAnchor("Required argument helper"); } guacdClientContent = guacdClientContent.replace( oldSendInstructionBlock, @@ -195,10 +218,7 @@ if ( !guacdClientContent.includes("opcode === 'required' || opcode === 'require'") ) { if (!guacdClientContent.includes(oldReadyHandler)) { - console.log( - "[patch-guacamole-lite] Required opcode target not found, skipping", - ); - process.exit(0); + missingAnchor("Required opcode"); } guacdClientContent = guacdClientContent.replace( oldReadyHandler, @@ -251,14 +271,93 @@ if (!cryptContent.includes(newDecryptBlock)) { newDecryptBlock, ); } else { - console.log( - "[patch-guacamole-lite] UTF-8 token decrypt target not found, skipping", - ); - process.exit(0); + missingAnchor("UTF-8 token decrypt"); } patched = true; } +// Patch 7: drop client-to-guacd input instructions from read-only session-share +// joins. guacd has no native read-only enforcement in the versions this project +// targets, so Termix must gate here. Denylist (not allowlist) on purpose: an +// unrecognized opcode is far more likely to be protocol plumbing (sync, blob, +// clipboard streams) than a new input vector, so failing open is the safer +// default for a client we already control. +const oldSendMessageToGuacd = + " sendMessageToGuacd(message) {\n" + + " this.lastActivity = Date.now();\n" + + " this.logger.log(LOGLEVEL.DEBUG, '[ >>> # ] Received from WS: ```' + message + '```');\n" + + "\n" + + " if (this.guacdClient) {\n" + + " this.guacdClient.send(message, true);\n" + + " }\n" + + " }"; +const newSendMessageToGuacd = + " sendMessageToGuacd(message) {\n" + + " this.lastActivity = Date.now();\n" + + " this.logger.log(LOGLEVEL.DEBUG, '[ >>> # ] Received from WS: ```' + message + '```');\n" + + "\n" + + " if (this.isReadOnlyJoin() && this.isInputInstruction(message)) {\n" + + " return;\n" + + " }\n" + + "\n" + + " if (this.guacdClient) {\n" + + " this.guacdClient.send(message, true);\n" + + " }\n" + + " }\n" + + "\n" + + " isReadOnlyJoin() {\n" + + " const connection = this.connectionSettings && this.connectionSettings.connection;\n" + + " return !!(connection && connection.join && connection.readOnly === true);\n" + + " }\n" + + "\n" + + " // Termix-only read-only gate, not part of the vendored library: extracts just\n" + + " // the leading opcode from a raw '.,...;' instruction without the\n" + + " // overhead of a full stateful parse.\n" + + " isInputInstruction(message) {\n" + + " const dot = message.indexOf('.');\n" + + " if (dot === -1) return false;\n" + + " const len = parseInt(message.substring(0, dot), 10);\n" + + " if (isNaN(len)) return false;\n" + + " const opcode = message.substring(dot + 1, dot + 1 + len);\n" + + " return ['mouse', 'key', 'touch', 'size'].includes(opcode);\n" + + " }"; + +if (!clientConnectionContent.includes("isReadOnlyJoin()")) { + if (!clientConnectionContent.includes(oldSendMessageToGuacd)) { + missingAnchor("sendMessageToGuacd"); + } + clientConnectionContent = clientConnectionContent.replace( + oldSendMessageToGuacd, + newSendMessageToGuacd, + ); + patched = true; +} + +// Patch 8: mergeConnectionOptions only preserves `join` across the settings +// merge, dropping Termix's `readOnly` flag before sendMessageToGuacd can see it. +const oldPreserveJoin = + " // For join connections, preserve the join property\n" + + " if (this.connectionSettings.connection.join) {\n" + + " compiledSettings.join = this.connectionSettings.connection.join;\n" + + " }"; +const newPreserveJoin = + " // For join connections, preserve the join property\n" + + " if (this.connectionSettings.connection.join) {\n" + + " compiledSettings.join = this.connectionSettings.connection.join;\n" + + " compiledSettings.readOnly = this.connectionSettings.connection.readOnly === true;\n" + + " }"; + +if (!clientConnectionContent.includes("compiledSettings.readOnly")) { + if (!clientConnectionContent.includes(oldPreserveJoin)) { + missingAnchor("join-preserve"); + } + clientConnectionContent = clientConnectionContent.replace( + oldPreserveJoin, + newPreserveJoin, + ); + patched = true; +} + if (!patched) { console.log("[patch-guacamole-lite] Already patched"); process.exit(0); @@ -266,6 +365,7 @@ if (!patched) { fs.writeFileSync(guacdClientPath, guacdClientContent); fs.writeFileSync(cryptPath, cryptContent); +fs.writeFileSync(clientConnectionPath, clientConnectionContent); console.log( - "[patch-guacamole-lite] Patched protocol VERSION_1_3_0/1_5_0 support, name handshake, required arguments, and UTF-8 token decrypt", + "[patch-guacamole-lite] Patched protocol VERSION_1_3_0/1_5_0 support, name handshake, required arguments, UTF-8 token decrypt, and read-only join input filtering", ); diff --git a/scripts/patch-guacamole-lite.test.ts b/scripts/patch-guacamole-lite.test.ts index d6b8c9e..70e3ad3 100644 --- a/scripts/patch-guacamole-lite.test.ts +++ b/scripts/patch-guacamole-lite.test.ts @@ -69,6 +69,31 @@ describe("patch-guacamole-lite", () => { ]); }); + it("sends name instruction for VERSION_1_1_0 to fix guacd 1.6.0 VNC drops", () => { + const client = createPatchedClient({ + hostname: "192.0.2.10", + port: 5900, + password: "secret", + width: 1280, + height: 720, + dpi: 96, + }); + + client.sendHandshakeReply(["VERSION_1_1_0", "hostname", "port"]); + + expect(client.sendInstruction).toHaveBeenCalledWith(["timezone"]); + expect(client.sendInstruction).toHaveBeenCalledWith([ + "name", + "guacamole-lite", + ]); + expect(client.sendInstruction).toHaveBeenCalledWith([ + "connect", + "VERSION_1_1_0", + "192.0.2.10", + 5900, + ]); + }); + it("answers required credentials through argument value streams", () => { const client = createPatchedClient({ username: "", diff --git a/scripts/patch-nan.cjs b/scripts/patch-nan.cjs index 5fec17f..43aad38 100644 --- a/scripts/patch-nan.cjs +++ b/scripts/patch-nan.cjs @@ -39,6 +39,19 @@ const nanHeaderPatched = patchFile(path.join(nanDir, "nan.h"), [ # define __builtin_frame_address(level) _AddressOfReturnAddress() #endif +// v8::External::New()/->Value() gained a mandatory ExternalPointerTypeTag +// argument in V8 15 (Electron 43+). Plain Node (V8 <= 13.x as of Node 24) +// still uses the old 2-arg signatures, so this must be conditional rather +// than assumed - a build can target either header set. +#include +#if defined(V8_MAJOR_VERSION) && V8_MAJOR_VERSION >= 15 +# define NAN_EXTERNAL_TAG_ARG , static_cast(0) +# define NAN_EXTERNAL_TAG_PARAM static_cast(0) +#else +# define NAN_EXTERNAL_TAG_ARG +# define NAN_EXTERNAL_TAG_PARAM +#endif + #define NODE_0_10_MODULE_VERSION 11`, }, ]); @@ -63,23 +76,24 @@ const bindingPatched = patchFile(bindingPath, [ }, ]); -// 2. nan_implementation_12_inl.h: replace v8::External::New() with the 3-arg form. -// Electron 42 / V8 13+ requires an ExternalPointerTypeTag as the third argument. +// 2. nan_implementation_12_inl.h: replace v8::External::New() with a form that +// passes NAN_EXTERNAL_TAG_ARG - a macro (defined in the nan.h patch above) +// that expands to the ExternalPointerTypeTag argument only when the target +// V8 headers actually declare it (V8 15+ / Electron 43+). const implPath = path.join(nanDir, "nan_implementation_12_inl.h"); let implPatched = false; if (fs.existsSync(implPath)) { let src = fs.readFileSync(implPath, "utf8"); const before = src; - const TAG = "static_cast(0)"; - if (!src.includes(TAG)) { + if (!src.includes("NAN_EXTERNAL_TAG_ARG")) { src = src.replace( - /v8::External::New\(v8::Isolate::GetCurrent\(\),\s*value\)/g, - `v8::External::New(v8::Isolate::GetCurrent(), value, ${TAG})`, + /v8::External::New\(v8::Isolate::GetCurrent\(\),\s*value(?:,\s*static_cast\(0\))?\)/g, + `v8::External::New(v8::Isolate::GetCurrent(), value NAN_EXTERNAL_TAG_ARG)`, ); src = src.replace( - /v8::External::New\(isolate,\s*reinterpret_cast\(callback\)\)/g, - `v8::External::New(isolate, reinterpret_cast(callback), ${TAG})`, + /v8::External::New\(isolate,\s*reinterpret_cast\(callback\)(?:,\s*static_cast\(0\))?\)/g, + `v8::External::New(isolate, reinterpret_cast(callback) NAN_EXTERNAL_TAG_ARG)`, ); } @@ -89,20 +103,19 @@ if (fs.existsSync(implPath)) { } } -// 3. nan_callbacks_12_inl.h: replace ->Value() with ->Value(tag) on v8::External. -// The new API requires an ExternalPointerTypeTag argument. +// 3. nan_callbacks_12_inl.h: replace ->Value() with ->Value(NAN_EXTERNAL_TAG_PARAM) +// on v8::External, same conditional-tag reasoning as above. const callbacksPath = path.join(nanDir, "nan_callbacks_12_inl.h"); let callbacksPatched = false; if (fs.existsSync(callbacksPath)) { let src = fs.readFileSync(callbacksPath, "utf8"); const before = src; - const TAG = "static_cast(0)"; - if (!src.includes(TAG)) { - // Pattern: .As()->Value()) โ€” always followed by )) + if (!src.includes("NAN_EXTERNAL_TAG_PARAM")) { + // Pattern: .As()->Value()) or ->Value()) src = src.replace( - /\.As\(\)->Value\(\)\)/g, - `.As()->Value(${TAG}))`, + /\.As\(\)->Value\((?:static_cast\(0\))?\)\)/g, + `.As()->Value(NAN_EXTERNAL_TAG_PARAM))`, ); } diff --git a/scripts/patch-xterm-android-ime.cjs b/scripts/patch-xterm-android-ime.cjs index e7738b3..4bbff4a 100644 --- a/scripts/patch-xterm-android-ime.cjs +++ b/scripts/patch-xterm-android-ime.cjs @@ -14,6 +14,17 @@ const xtermDir = path.join( // xtermjs/xterm.js#3600 remains unresolved upstream. Android IMEs can restart // composition on the previous word and replace it with a shorter value (for // example, Vietnamese "Hoar" -> "Hแปa"). xterm 6.0 otherwise emits nothing. +// +// Also fixes _handleAnyTextareaChanges, which iOS Safari/WKWebView drives +// for ordinary typing (it reports keyCode 229 for all software-keyboard +// input, not just IME composition). That handler diffs the textarea value +// via `newValue.replace(oldValue, "")`, a literal substring removal. When +// keystrokes arrive faster than the function's setTimeout(0) callback runs, +// several overlapping callbacks each capture a stale oldValue, so the +// literal-substring search fails to match and the diff silently comes back +// empty - characters are dropped instead of sent. Swap in the same +// common-prefix diff used for composition-end above so a stale oldValue +// still yields the correct delta. const patches = [ { file: "xterm.mjs", @@ -34,6 +45,10 @@ const patches = [ "e.start+=this._dataAlreadySent.length,this._isComposing?i=this._textarea.value.substring(e.start,this._compositionPosition.start):i=this._textarea.value.substring(e.start),i.length>0&&", "e.start+=this._dataAlreadySent.length;if(this._isComposing)i=this._textarea.value.substring(e.start,this._compositionPosition.start);else{const t=this._textarea.value;if(t.length0&&", ], + [ + '_handleAnyTextareaChanges(){let t=this._textarea.value;setTimeout(()=>{if(!this._isComposing){let e=this._textarea.value,i=e.replace(t,"");this._dataAlreadySent=i,e.length>t.length?this._coreService.triggerDataEvent(i,!0):e.length{if(!this._isComposing){let e=this._textarea.value,r=0;const n=Math.min(e.length,t.length);for(;r0&&this._coreService.triggerDataEvent(i,!0)}},0)}", + ], ], }, { @@ -55,6 +70,10 @@ const patches = [ "e.start+=this._dataAlreadySent.length,t=this._isComposing?this._textarea.value.substring(e.start,this._compositionPosition.start):this._textarea.value.substring(e.start),t.length>0&&", "e.start+=this._dataAlreadySent.length;this._isComposing?t=this._textarea.value.substring(e.start,this._compositionPosition.start):(()=>{const s=this._textarea.value;if(s.length0&&", ], + [ + '_handleAnyTextareaChanges(){const e=this._textarea.value;setTimeout((()=>{if(!this._isComposing){const t=this._textarea.value,i=t.replace(e,"");this._dataAlreadySent=i,t.length>e.length?this._coreService.triggerDataEvent(i,!0):t.length{if(!this._isComposing){const t=this._textarea.value;let r=0;const n=Math.min(t.length,e.length);for(;r0&&this._coreService.triggerDataEvent(i,!0)}}),0)}", + ], ], }, ]; @@ -66,18 +85,24 @@ for (const { file, replacements } of patches) { } let source = fs.readFileSync(filePath, "utf8"); - if (source.includes("_preCompositionValue")) { - console.log(`[patch-xterm-android-ime] ${file} already patched`); - continue; - } + let changed = false; for (const [original, patched] of replacements) { + if (source.includes(patched)) { + continue; + } if (!source.includes(original)) { throw new Error( `[patch-xterm-android-ime] Expected source not found in ${file}`, ); } source = source.replace(original, patched); + changed = true; + } + + if (!changed) { + console.log(`[patch-xterm-android-ime] ${file} already patched`); + continue; } fs.writeFileSync(filePath, source); diff --git a/scripts/verify-dialects.mjs b/scripts/verify-dialects.mjs new file mode 100644 index 0000000..308a530 --- /dev/null +++ b/scripts/verify-dialects.mjs @@ -0,0 +1,140 @@ +/** + * Runs the repository layer against a real Postgres or MySQL server. + * + * The unit tests only ever see SQLite, so the parts of this codebase that + * differ per engine โ€” the RETURNING replacements, the read-then-write + * transactions, the value encoders โ€” have no coverage there at all. This is + * what covers them, and it needs a live server, which is why it is a script + * rather than a test. + * + * Usage: + * npm run verify:dialect -- postgres://user:pass@host:5432/db + * npm run verify:dialect -- mysql://user:pass@host:3306/db + * + * Applies the migrations first, through the same runRemoteMigrations() the + * application uses at startup โ€” so a broken migration fails here rather than in + * production. Writes real rows: point it at a scratch database. + */ + +import { randomUUID } from "crypto"; + +const url = process.argv[2]; +if (!url) { + console.error("usage: node scripts/verify-dialects.mjs "); + process.exit(2); +} + +const scheme = url.split("://", 1)[0].toLowerCase(); +const dialect = scheme.startsWith("postgres") + ? "postgres" + : scheme === "mysql" || scheme === "mariadb" + ? "mysql" + : null; + +if (!dialect) { + console.error(`unsupported URL scheme "${scheme}://"`); + process.exit(2); +} + +const { drizzle } = await import( + dialect === "postgres" ? "drizzle-orm/node-postgres" : "drizzle-orm/mysql2" +); + +// No schema option on purpose โ€” see connect.ts. +const db = drizzle(url); +const context = { dialect, drizzle: db }; + +const { runRemoteMigrations } = + await import("../src/backend/database/db/migrate.js"); +await runRemoteMigrations(dialect, db); + +const { UserRepository } = + await import("../src/backend/database/repositories/user-repository.js"); +const { HostRepository } = + await import("../src/backend/database/repositories/host-repository.js"); +const { SettingsRepository } = + await import("../src/backend/database/repositories/settings-repository.js"); + +let failures = 0; +const check = (label, got, want) => { + const ok = JSON.stringify(got) === JSON.stringify(want); + if (!ok) failures++; + console.log( + ` ${ok ? "ok " : "FAIL"} ${label}` + + (ok + ? "" + : `\n got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`), + ); +}; + +console.log(`\nverifying ${dialect} at ${url.replace(/:[^:@]*@/, ":***@")}\n`); + +const users = new UserRepository(context); +const userId = `verify-${randomUUID()}`; + +// insertReturning: on MySQL this is an insert plus a read inside a transaction. +const created = await users.create({ + id: userId, + username: "before", + passwordHash: "x", + isAdmin: true, +}); +check("insert returns the stored row", created?.username, "before"); + +// The one non-identity value encoder in the schema. Booleans are integers in +// the sqlite definitions the repositories import, so this asserts that 1/0 +// survives a round trip through a native boolean column. +check("boolean true survives the round trip", created?.isAdmin, true); + +// updateReturning must report the state AFTER the write. Reading first would +// return the value the update replaced โ€” silently, with no error. +const updated = await users.update(userId, { username: "after" }); +check("update returns the new value", updated?.username, "after"); + +const hosts = new HostRepository(context); +const host = await hosts.create({ + userId, + name: "verify", + ip: "127.0.0.1", + port: 22, + username: "root", + authType: "password", + enableTerminal: true, +}); +check( + "autoincrement id came back", + typeof host?.id === "number" && host.id > 0, + true, +); +check( + "database-assigned createdAt came back", + typeof host?.createdAt === "string" && host.createdAt.length > 0, + true, +); + +// deleteReturning must report the state BEFORE the write. Reading afterwards +// would find nothing at all. +const settings = new SettingsRepository(context); +const prefix = `verify-${randomUUID()}`; +await settings.set(`${prefix}-a`, "1"); +await settings.set(`${prefix}-b`, "2"); +check( + "delete reports the rows it removed", + await settings.deleteLike(`${prefix}-%`), + 2, +); +check( + "and they are actually gone", + (await settings.listAll()).filter((row) => row.key.startsWith(prefix)).length, + 0, +); + +await hosts.deleteForUser(userId, host.id); +check("host really deleted", await hosts.findById(host.id), null); + +console.log( + failures === 0 + ? `\n${dialect}: all checks passed\n` + : `\n${dialect}: ${failures} FAILED\n`, +); +process.exit(failures === 0 ? 0 : 1); diff --git a/src/backend/ai/context.ts b/src/backend/ai/context.ts new file mode 100644 index 0000000..ce8fc40 --- /dev/null +++ b/src/backend/ai/context.ts @@ -0,0 +1,55 @@ +/** + * The system prompt. + * + * Deliberately small: the assistant starts with almost no context and must call + * a read tool to learn anything. That keeps token cost predictable, makes every + * data access visible in the transcript, and means nothing is sent to a + * third-party provider that the user did not implicitly ask for. + */ +export function buildSystemPrompt(options: { + hostCount: number; + activeTab?: string | null; + allowReadOnlyCommands: boolean; +}): string { + const lines: string[] = [ + "You are the assistant built into Termix, a self-hosted server management app.", + "You help the user manage their servers, snippets, automations, fleets and alerts.", + "", + "How you work:", + "- You start with no knowledge of this user's setup. Call a read tool to find out anything you need.", + "- You cannot change anything directly. To make a change, call a propose_* tool; the user sees a card and approves or rejects it.", + "- You have no access to passwords, SSH keys, API keys or any other credential, and you never ask the user to paste one into the chat.", + "", + "Scope:", + "- Do exactly what was asked, and nothing beyond it. A question is a request for an answer, not for changes.", + "- Questions like 'what is running on this server', 'check this host' or 'why is this slow' are answered with information. Read, then report. Do not propose anything.", + "- Only propose a change when the user asked for one, in words like 'add', 'create', 'set up', 'fix' or 'change'.", + "- Do not propose follow-up work you thought of yourself: no monitoring, no alert rules, no scripts, no snippets, no cleanup, unless that is what was requested.", + "- If you think something is worth doing, say so in one sentence and stop. Let the user ask.", + "- One request means one proposal at most. Do not bundle extras alongside it.", + "", + "How to answer:", + "- Lead with the answer. Keep responses short and concrete.", + "- When you propose something, say in one line what it does and why.", + "- If a request is ambiguous in a way that changes what you would propose, ask before proposing.", + "- Never claim you have done something. You propose; the user applies.", + ]; + + if (options.allowReadOnlyCommands) { + lines.push( + "- The user has allowed you to run read-only diagnostic commands directly. Anything that changes state still has to be proposed.", + ); + } + + lines.push( + "", + "Current context:", + `- The user has ${options.hostCount} host${options.hostCount === 1 ? "" : "s"} configured.`, + ); + + if (options.activeTab) { + lines.push(`- They are currently looking at: ${options.activeTab}.`); + } + + return lines.join("\n"); +} diff --git a/src/backend/ai/egress.ts b/src/backend/ai/egress.ts new file mode 100644 index 0000000..700b11c --- /dev/null +++ b/src/backend/ai/egress.ts @@ -0,0 +1,113 @@ +import { isIP } from "net"; +import { isBlockedAddress } from "../utils/safe-outbound-fetch.js"; +import { createCurrentSettingsRepository } from "../database/repositories/factory.js"; + +/** + * Where the assistant is allowed to send requests. + * + * Cloud providers are reached through the SSRF-guarded path, which refuses to + * resolve to a private address. That guard is exactly what a self-hosted Ollama + * on localhost trips over, so private destinations are permitted only when an + * admin has named the host. Without that split, any logged-in user could point + * a "provider" at an internal service and use the backend as an authenticated + * probe of the server's own network. + */ + +export const AI_PRIVATE_ALLOWLIST_KEY = "ai_private_endpoint_allowlist"; + +/** Hosts a self-hoster almost certainly wants, and which reach only this machine. */ +export const DEFAULT_PRIVATE_ALLOWLIST = [ + "localhost", + "127.0.0.1", + "::1", + "host.docker.internal", +]; + +export function parseAllowlist(raw: string | null): string[] { + if (!raw) return [...DEFAULT_PRIVATE_ALLOWLIST]; + try { + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return [...DEFAULT_PRIVATE_ALLOWLIST]; + return parsed + .filter((entry): entry is string => typeof entry === "string") + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean); + } catch { + return [...DEFAULT_PRIVATE_ALLOWLIST]; + } +} + +export async function readPrivateAllowlist(): Promise { + const raw = await createCurrentSettingsRepository().get( + AI_PRIVATE_ALLOWLIST_KEY, + ); + return parseAllowlist(raw); +} + +function normalizeHost(hostname: string): string { + return hostname.replace(/^\[|\]$/g, "").toLowerCase(); +} + +/** + * True when the URL names a destination the SSRF guard would refuse. A bare + * hostname that is not an IP literal (e.g. "ollama.internal") is treated as + * private only if it is "localhost" -- anything else resolves through DNS and + * is caught at connect time by the guard instead. + */ +export function isPrivateDestination(rawUrl: string): boolean { + let url: URL; + try { + url = new URL(rawUrl); + } catch { + return false; + } + const host = normalizeHost(url.hostname); + if (host === "localhost" || host.endsWith(".localhost")) return true; + if (isIP(host)) return isBlockedAddress(host); + return false; +} + +export interface EgressDecision { + allowed: boolean; + /** True when the destination needs the allowlisted-private path. */ + isPrivate: boolean; + reason?: string; +} + +export function evaluateEgress( + rawUrl: string, + allowlist: string[], +): EgressDecision { + let url: URL; + try { + url = new URL(rawUrl); + } catch { + return { allowed: false, isPrivate: false, reason: "Invalid URL" }; + } + + if (!["http:", "https:"].includes(url.protocol)) { + return { allowed: false, isPrivate: false, reason: "Unsupported protocol" }; + } + if (url.username || url.password) { + return { + allowed: false, + isPrivate: false, + reason: "Credentials in URL are not allowed", + }; + } + + const host = normalizeHost(url.hostname); + const isPrivate = isPrivateDestination(rawUrl); + + if (!isPrivate) return { allowed: true, isPrivate: false }; + + const normalized = allowlist.map((entry) => entry.trim().toLowerCase()); + if (normalized.includes(host)) return { allowed: true, isPrivate: true }; + + return { + allowed: false, + isPrivate: true, + reason: + "This address is on a private network. An administrator must add its host to the AI endpoint allowlist first.", + }; +} diff --git a/src/backend/ai/engine.ts b/src/backend/ai/engine.ts new file mode 100644 index 0000000..17aaed0 --- /dev/null +++ b/src/backend/ai/engine.ts @@ -0,0 +1,150 @@ +import { getErrorMessage } from "../utils/error-message.js"; +import { getAdapter } from "./providers/registry.js"; +import type { + ChatMessage, + ProviderConfig, + ToolCall, +} from "./providers/types.js"; +import { redact, redactToJson } from "./redaction.js"; +import { getTool, toolDefinitions } from "./tools/catalog.js"; +import { + isProposalDraft, + type ProposalDraft, + type ToolContext, +} from "./tools/types.js"; + +/** + * The agent loop: stream a turn, run any tools the model asked for, feed the + * results back, repeat. Bounded so a model that keeps calling tools cannot spin + * forever. + */ + +const MAX_TURNS = 8; + +export type EngineEvent = + | { type: "token"; text: string } + | { type: "tool_call"; name: string; arguments: Record } + | { type: "tool_result"; name: string; result: unknown } + | { type: "proposal"; draft: ProposalDraft } + | { type: "assistant_message"; content: string; toolCalls: ToolCall[] } + | { type: "done" } + | { type: "error"; message: string }; + +export interface EngineOptions { + config: ProviderConfig; + model: string; + system: string; + history: ChatMessage[]; + context: ToolContext; + signal?: AbortSignal; +} + +export async function* runAgent( + options: EngineOptions, +): AsyncGenerator { + const adapter = getAdapter(options.config.providerType); + const tools = toolDefinitions(); + const messages: ChatMessage[] = [...options.history]; + + for (let turn = 0; turn < MAX_TURNS; turn += 1) { + let text = ""; + const calls: ToolCall[] = []; + let failed = false; + + try { + for await (const chunk of adapter.streamChat(options.config, { + model: options.model, + system: options.system, + messages, + tools, + signal: options.signal, + })) { + if (chunk.type === "text") { + text += chunk.text; + yield { type: "token", text: chunk.text }; + } else if (chunk.type === "tool_call") { + calls.push(chunk.call); + } else if (chunk.type === "error") { + failed = true; + yield { type: "error", message: chunk.message }; + } + } + } catch (error) { + const message = getErrorMessage(error, "The provider request failed"); + yield { type: "error", message }; + return; + } + + if (failed) return; + + yield { type: "assistant_message", content: text, toolCalls: calls }; + + if (!calls.length) { + yield { type: "done" }; + return; + } + + messages.push({ role: "assistant", content: text, toolCalls: calls }); + + for (const call of calls) { + yield { type: "tool_call", name: call.name, arguments: call.arguments }; + + const result = await runTool(call, options.context); + + if (isProposalDraft(result)) { + // Closes the tool call before the proposal card is emitted. Without + // this the call has no matching result and renders as permanently + // running, even though the work is done and awaiting the user. + yield { + type: "tool_result", + name: call.name, + result: { status: "awaiting_user_approval" }, + }; + yield { type: "proposal", draft: result }; + // The model is told the proposal is awaiting the user rather than done, + // so it does not go on to describe the change as applied. + messages.push({ + role: "tool", + content: JSON.stringify({ + status: "awaiting_user_approval", + summary: result.summary, + }), + toolCallId: call.id, + toolName: call.name, + }); + continue; + } + + yield { type: "tool_result", name: call.name, result: redact(result) }; + messages.push({ + role: "tool", + content: redactToJson(result), + toolCallId: call.id, + toolName: call.name, + }); + } + } + + // Ran out of turns with the model still calling tools. + yield { + type: "error", + message: "The assistant used too many steps without finishing.", + }; +} + +async function runTool(call: ToolCall, context: ToolContext): Promise { + const tool = getTool(call.name); + + // A model can emit any name it likes; only the catalog decides what runs. + if (!tool) { + return { error: `Unknown tool: ${call.name}` }; + } + + try { + return await tool.handler(call.arguments ?? {}, context); + } catch (error) { + return { + error: getErrorMessage(error, "The tool failed"), + }; + } +} diff --git a/src/backend/ai/gating.ts b/src/backend/ai/gating.ts new file mode 100644 index 0000000..689363a --- /dev/null +++ b/src/backend/ai/gating.ts @@ -0,0 +1,70 @@ +import type { NextFunction, Response } from "express"; +import type { AuthenticatedRequest } from "../../types/index.js"; +import { + createCurrentSettingsRepository, + createCurrentUserPreferenceRepository, +} from "../database/repositories/factory.js"; + +/** + * Three gates, all checked on the server. + * + * The admin global is a hard kill switch: when it is off the feature does not + * exist for anyone, regardless of what any user has enabled. It defaults to + * false so upgrading an existing install turns nothing on by surprise. + * + * Mirrors the shape of isSharingEnabledForHost in the session-sharing routes, + * where the global also wins over the per-entity setting. + */ + +export const AI_GLOBAL_ENABLED_KEY = "ai_globally_enabled"; + +export async function isAiGloballyEnabled(): Promise { + return createCurrentSettingsRepository().getBoolean( + AI_GLOBAL_ENABLED_KEY, + false, + ); +} + +export interface AiAccess { + enabled: boolean; + allowReadOnlyCommands: boolean; +} + +export async function resolveAiAccess(userId: string): Promise { + const globalEnabled = await isAiGloballyEnabled(); + if (!globalEnabled) { + return { enabled: false, allowReadOnlyCommands: false }; + } + + const preferences = + await createCurrentUserPreferenceRepository().findByUserId(userId); + + return { + // Null means the user was never asked, which is not consent. + enabled: preferences?.aiAssistantEnabled === true, + allowReadOnlyCommands: preferences?.aiReadOnlyCommands === true, + }; +} + +/** Rejects any AI request unless both gates are open. */ +export function createAiGate() { + return async ( + req: AuthenticatedRequest, + res: Response, + next: NextFunction, + ): Promise => { + if (!req.userId) { + res.status(401).json({ error: "Authentication required" }); + return; + } + + const access = await resolveAiAccess(req.userId); + if (!access.enabled) { + res.status(403).json({ error: "The AI assistant is not enabled" }); + return; + } + + (req as AuthenticatedRequest & { aiAccess?: AiAccess }).aiAccess = access; + next(); + }; +} diff --git a/src/backend/ai/index.ts b/src/backend/ai/index.ts new file mode 100644 index 0000000..aa31f60 --- /dev/null +++ b/src/backend/ai/index.ts @@ -0,0 +1,976 @@ +import { getErrorMessage } from "../utils/error-message.js"; +import express from "express"; +import type { AuthenticatedRequest } from "../../types/index.js"; +import { AuthManager } from "../utils/auth-manager.js"; +import { databaseLogger } from "../utils/logger.js"; +import { + getAuditUsername, + getRequestMeta, + logAudit, +} from "../utils/audit-logger.js"; +import { + createCurrentAiRepository, + createCurrentHostRepository, +} from "../database/repositories/factory.js"; +import { buildSystemPrompt } from "./context.js"; +import { runAgent } from "./engine.js"; +import { + createAiGate, + isAiGloballyEnabled, + resolveAiAccess, +} from "./gating.js"; +import { + FALLBACK_MODELS, + getAdapter, + REQUIRES_API_KEY, + REQUIRES_BASE_URL, +} from "./providers/registry.js"; +import type { + AiProviderType, + ChatMessage, + ProviderConfig, +} from "./providers/types.js"; +import { isAiProviderType } from "./providers/types.js"; +import { applyProposal } from "./tools/executor.js"; + +const router = express.Router(); + +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); +const requireDataAccess = authManager.createDataAccessMiddleware(); +const aiGate = createAiGate(); + +function parseId(raw: unknown): number | null { + const id = typeof raw === "string" ? parseInt(raw, 10) : Number(raw); + return Number.isInteger(id) && id > 0 ? id : null; +} + +/** + * @openapi + * /ai/status: + * get: + * summary: Whether the AI assistant is available to this user + * description: > + * Deliberately not behind the AI gate: the frontend calls this to decide + * whether to render any AI surface at all, and needs a plain answer rather + * than a 403 when the feature is off. + * tags: + * - AI + * responses: + * 200: + * description: The effective enablement state. + */ +router.get("/status", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + try { + const [globalEnabled, access] = await Promise.all([ + isAiGloballyEnabled(), + resolveAiAccess(userId), + ]); + res.json({ + globallyEnabled: globalEnabled, + enabled: access.enabled, + allowReadOnlyCommands: access.allowReadOnlyCommands, + }); + } catch (err) { + databaseLogger.error("Failed to read AI status", err, { + operation: "ai_status_failed", + userId, + }); + res.status(500).json({ error: "Failed to read AI status" }); + } +}); + +/** + * @openapi + * /ai/providers: + * get: + * summary: List the user's configured AI providers + * tags: + * - AI + * responses: + * 200: + * description: Providers, with API keys masked. + * 403: + * description: The AI assistant is not enabled. + */ +router.get( + "/providers", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + try { + const providers = await createCurrentAiRepository().listProviders(userId); + res.json({ providers }); + } catch (err) { + databaseLogger.error("Failed to list AI providers", err, { + operation: "ai_providers_list_failed", + userId, + }); + res.status(500).json({ error: "Failed to list providers" }); + } + }, +); + +/** + * @openapi + * /ai/providers: + * post: + * summary: Add an AI provider + * tags: + * - AI + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * providerType: + * type: string + * label: + * type: string + * baseUrl: + * type: string + * apiKey: + * type: string + * defaultModel: + * type: string + * responses: + * 201: + * description: Provider created. + * 400: + * description: Invalid request body. + */ +router.post( + "/providers", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const { providerType, label, baseUrl, apiKey, defaultModel } = + req.body ?? {}; + + if (!isAiProviderType(providerType)) { + return res.status(400).json({ error: "Unknown provider type" }); + } + if (typeof label !== "string" || !label.trim()) { + return res.status(400).json({ error: "label is required" }); + } + if (REQUIRES_BASE_URL.includes(providerType) && !baseUrl?.trim()) { + return res.status(400).json({ error: "This provider needs a base URL" }); + } + if (REQUIRES_API_KEY.includes(providerType) && !apiKey?.trim()) { + return res.status(400).json({ error: "This provider needs an API key" }); + } + + try { + const created = await createCurrentAiRepository().createProvider({ + userId, + providerType, + label: label.trim(), + baseUrl: typeof baseUrl === "string" ? baseUrl.trim() : null, + apiKey: typeof apiKey === "string" ? apiKey.trim() : null, + defaultModel: + typeof defaultModel === "string" ? defaultModel.trim() : null, + }); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "create_ai_provider", + resourceType: "ai_provider", + resourceId: String(created.id), + resourceName: created.label, + ipAddress, + userAgent, + success: true, + }); + + res.status(201).json({ provider: created }); + } catch (err) { + databaseLogger.error("Failed to create AI provider", err, { + operation: "ai_provider_create_failed", + userId, + }); + res.status(500).json({ error: "Failed to create provider" }); + } + }, +); + +/** + * @openapi + * /ai/providers/{id}: + * patch: + * summary: Update an AI provider + * tags: + * - AI + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Provider updated. + * 404: + * description: Provider not found. + */ +router.patch( + "/providers/:id", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const id = parseId(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid provider id" }); + + try { + const updated = await createCurrentAiRepository().updateProvider( + id, + userId, + req.body ?? {}, + ); + if (!updated) + return res.status(404).json({ error: "Provider not found" }); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "update_ai_provider", + resourceType: "ai_provider", + resourceId: String(id), + resourceName: updated.label, + ipAddress, + userAgent, + success: true, + }); + + res.json({ provider: updated }); + } catch (err) { + databaseLogger.error("Failed to update AI provider", err, { + operation: "ai_provider_update_failed", + userId, + }); + res.status(500).json({ error: "Failed to update provider" }); + } + }, +); + +/** + * @openapi + * /ai/providers/{id}: + * delete: + * summary: Delete an AI provider + * tags: + * - AI + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Provider deleted. + * 404: + * description: Provider not found. + */ +router.delete( + "/providers/:id", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const id = parseId(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid provider id" }); + + try { + const deleted = await createCurrentAiRepository().deleteProvider( + id, + userId, + ); + if (!deleted) + return res.status(404).json({ error: "Provider not found" }); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "delete_ai_provider", + resourceType: "ai_provider", + resourceId: String(id), + ipAddress, + userAgent, + success: true, + }); + + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to delete AI provider", err, { + operation: "ai_provider_delete_failed", + userId, + }); + res.status(500).json({ error: "Failed to delete provider" }); + } + }, +); + +/** + * @openapi + * /ai/probe-models: + * post: + * summary: List models for a provider that has not been saved yet + * description: > + * Lets the add-provider form fill its model picker before the provider + * exists, so nobody has to go and look up model names by hand. + * tags: + * - AI + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * providerType: + * type: string + * baseUrl: + * type: string + * apiKey: + * type: string + * providerId: + * type: integer + * responses: + * 200: + * description: Model ids, possibly a curated fallback list. + * 400: + * description: Unknown provider type. + */ +router.post( + "/probe-models", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const { providerType, baseUrl, apiKey, providerId } = req.body ?? {}; + + if (!isAiProviderType(providerType)) { + return res.status(400).json({ error: "Unknown provider type" }); + } + + try { + // Editing an existing provider sends no key, so fall back to the stored + // one rather than making the user retype it just to refresh the list. + let resolvedKey = + typeof apiKey === "string" && apiKey.trim() ? apiKey.trim() : null; + if (!resolvedKey && parseId(providerId)) { + const stored = await createCurrentAiRepository().findProviderWithSecret( + parseId(providerId) as number, + userId, + ); + resolvedKey = stored?.apiKey ?? null; + } + + const models = await getAdapter(providerType).listModels({ + providerType, + baseUrl: typeof baseUrl === "string" ? baseUrl.trim() : null, + apiKey: resolvedKey, + }); + + res.json({ models, source: "live" }); + } catch (err) { + // A provider that cannot be reached yet still gets a usable list, so the + // form is never a blank text box the user has to guess into. + const fallback = FALLBACK_MODELS[providerType as AiProviderType] ?? []; + res.json({ + models: fallback, + source: fallback.length ? "fallback" : "none", + warning: err instanceof Error ? err.message : undefined, + }); + } + }, +); + +/** + * @openapi + * /ai/providers/{id}/models: + * get: + * summary: List models available from a provider + * tags: + * - AI + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Model ids. + * 502: + * description: The provider could not be reached. + */ +router.get( + "/providers/:id/models", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const id = parseId(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid provider id" }); + + try { + const provider = await createCurrentAiRepository().findProviderWithSecret( + id, + userId, + ); + if (!provider) + return res.status(404).json({ error: "Provider not found" }); + + const models = await getAdapter(provider.providerType).listModels({ + providerType: provider.providerType as ProviderConfig["providerType"], + baseUrl: provider.baseUrl, + apiKey: provider.apiKey, + }); + res.json({ models }); + } catch (err) { + // The message can carry the allowlist hint, which the user needs to act on. + const message = getErrorMessage(err, "Could not reach the provider"); + databaseLogger.warn("Failed to list provider models", { + operation: "ai_provider_models_failed", + userId, + }); + res.status(502).json({ error: message }); + } + }, +); + +/** + * @openapi + * /ai/conversations: + * get: + * summary: List the user's AI conversations + * tags: + * - AI + * responses: + * 200: + * description: Conversations, newest first. + */ +router.get( + "/conversations", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + try { + const conversations = + await createCurrentAiRepository().listConversations(userId); + res.json({ conversations }); + } catch (err) { + databaseLogger.error("Failed to list AI conversations", err, { + operation: "ai_conversations_list_failed", + userId, + }); + res.status(500).json({ error: "Failed to list conversations" }); + } + }, +); + +/** + * @openapi + * /ai/conversations/{id}: + * get: + * summary: Get one conversation with its messages and proposals + * tags: + * - AI + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The conversation. + * 404: + * description: Conversation not found. + */ +router.get( + "/conversations/:id", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const id = parseId(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid conversation id" }); + + try { + const repository = createCurrentAiRepository(); + const conversation = await repository.findConversation(id, userId); + if (!conversation) { + return res.status(404).json({ error: "Conversation not found" }); + } + + const [messages, proposals] = await Promise.all([ + repository.listMessages(id), + repository.listProposals(userId, id), + ]); + + res.json({ conversation, messages, proposals }); + } catch (err) { + databaseLogger.error("Failed to load AI conversation", err, { + operation: "ai_conversation_load_failed", + userId, + }); + res.status(500).json({ error: "Failed to load conversation" }); + } + }, +); + +/** + * @openapi + * /ai/conversations/{id}: + * delete: + * summary: Delete a conversation + * tags: + * - AI + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Conversation deleted. + */ +router.delete( + "/conversations/:id", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const id = parseId(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid conversation id" }); + + try { + const deleted = await createCurrentAiRepository().deleteConversation( + id, + userId, + ); + if (!deleted) { + return res.status(404).json({ error: "Conversation not found" }); + } + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to delete AI conversation", err, { + operation: "ai_conversation_delete_failed", + userId, + }); + res.status(500).json({ error: "Failed to delete conversation" }); + } + }, +); + +/** + * @openapi + * /ai/chat/stream: + * post: + * summary: Send a message and stream the assistant's reply + * description: > + * Server-sent events. Emits token, tool_call, tool_result, proposal, + * done and error frames. + * tags: + * - AI + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * conversationId: + * type: integer + * providerId: + * type: integer + * model: + * type: string + * message: + * type: string + * activeTab: + * type: string + * responses: + * 200: + * description: An event stream. + * 400: + * description: Invalid request body. + */ +router.post( + "/chat/stream", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const { conversationId, providerId, model, message, activeTab } = + req.body ?? {}; + + if (typeof message !== "string" || !message.trim()) { + return res.status(400).json({ error: "message is required" }); + } + + const resolvedProviderId = parseId(providerId); + if (!resolvedProviderId) { + return res.status(400).json({ error: "providerId is required" }); + } + + const repository = createCurrentAiRepository(); + + try { + const provider = await repository.findProviderWithSecret( + resolvedProviderId, + userId, + ); + if (!provider) { + return res.status(404).json({ error: "Provider not found" }); + } + + const chosenModel = + (typeof model === "string" && model.trim()) || + provider.defaultModel || + ""; + if (!chosenModel) { + return res.status(400).json({ error: "No model selected" }); + } + + // Resolve or create the conversation before the stream opens, so a + // failure here is still a normal JSON error the client can render. + let conversation = conversationId + ? await repository.findConversation(Number(conversationId), userId) + : null; + if (!conversation) { + conversation = await repository.createConversation({ + userId, + title: message.trim().slice(0, 60), + providerId: resolvedProviderId, + model: chosenModel, + }); + } + + const history = await repository.listMessages(conversation.id); + await repository.appendMessage({ + conversationId: conversation.id, + role: "user", + content: message.trim(), + }); + + const access = await resolveAiAccess(userId); + const hosts = await createCurrentHostRepository().listByUserId(userId); + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-store, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }); + res.flushHeaders?.(); + + const heartbeat = setInterval(() => { + try { + res.write(": keepalive\n\n"); + } catch { + clearInterval(heartbeat); + } + }, 30000); + + const abort = new AbortController(); + req.on("close", () => { + clearInterval(heartbeat); + abort.abort(); + }); + + const send = (event: unknown) => { + res.write(`data: ${JSON.stringify(event)}\n\n`); + }; + + send({ type: "conversation", conversationId: conversation.id }); + + const chatHistory: ChatMessage[] = history.map((entry) => ({ + role: entry.role as ChatMessage["role"], + content: entry.content, + ...(entry.toolCalls ? { toolCalls: JSON.parse(entry.toolCalls) } : {}), + })); + chatHistory.push({ role: "user", content: message.trim() }); + + let assistantText = ""; + let assistantToolCalls: unknown[] = []; + + try { + for await (const event of runAgent({ + config: { + providerType: + provider.providerType as ProviderConfig["providerType"], + baseUrl: provider.baseUrl, + apiKey: provider.apiKey, + }, + model: chosenModel, + system: buildSystemPrompt({ + hostCount: hosts.length, + activeTab: typeof activeTab === "string" ? activeTab : null, + allowReadOnlyCommands: access.allowReadOnlyCommands, + }), + history: chatHistory, + context: { + userId, + conversationId: conversation.id, + allowReadOnlyCommands: access.allowReadOnlyCommands, + }, + signal: abort.signal, + })) { + if (event.type === "assistant_message") { + assistantText = event.content; + // Kept so the next message replays them verbatim. Gemini rejects a + // turn whose functionCall parts lost their thoughtSignature, so + // dropping these breaks the second message in every conversation. + assistantToolCalls = event.toolCalls; + continue; + } + + if (event.type === "proposal") { + const stored = await repository.createProposal({ + conversationId: conversation.id, + userId, + kind: event.draft.kind, + summary: event.draft.summary, + payload: JSON.stringify(event.draft.payload), + }); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "ai_proposal_created", + resourceType: "ai_proposal", + resourceId: String(stored.id), + resourceName: event.draft.kind, + ipAddress, + userAgent, + success: true, + }); + + send({ type: "proposal", proposal: stored }); + continue; + } + + send(event); + } + } finally { + clearInterval(heartbeat); + } + + if (assistantText || assistantToolCalls.length) { + await repository.appendMessage({ + conversationId: conversation.id, + role: "assistant", + content: assistantText, + toolCalls: assistantToolCalls.length + ? JSON.stringify(assistantToolCalls) + : null, + }); + } + await repository.touchConversation(conversation.id); + + send({ type: "done" }); + res.end(); + } catch (err) { + databaseLogger.error("AI chat stream failed", err, { + operation: "ai_chat_stream_failed", + userId, + }); + if (res.headersSent) { + res.write( + `data: ${JSON.stringify({ type: "error", message: "The assistant stopped unexpectedly" })}\n\n`, + ); + res.end(); + } else { + res.status(500).json({ error: "Failed to start the assistant" }); + } + } + }, +); + +/** + * @openapi + * /ai/proposals/{id}/apply: + * post: + * summary: Apply a pending proposal + * description: > + * Re-validates the stored payload and dispatches it through the same + * repository logic a manual action uses. + * tags: + * - AI + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Proposal applied. + * 400: + * description: The proposal could not be applied. + * 404: + * description: Proposal not found. + */ +router.post( + "/proposals/:id/apply", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const id = parseId(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid proposal id" }); + + const repository = createCurrentAiRepository(); + + try { + const stored = await repository.findProposal(id, userId); + if (!stored) return res.status(404).json({ error: "Proposal not found" }); + if (stored.status !== "pending") { + return res + .status(400) + .json({ error: `This proposal was already ${stored.status}` }); + } + + let payload: Record; + try { + payload = JSON.parse(stored.payload) as Record; + } catch { + return res + .status(400) + .json({ error: "The proposal payload is invalid" }); + } + + const result = await applyProposal(stored.kind, payload, userId); + await repository.setProposalStatus(id, userId, "applied", result.summary); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "ai_proposal_applied", + resourceType: "ai_proposal", + resourceId: String(id), + resourceName: stored.kind, + ipAddress, + userAgent, + success: true, + }); + + res.json({ success: result.ok, summary: result.summary }); + } catch (err) { + const message = getErrorMessage(err, "Failed to apply the proposal"); + databaseLogger.error("Failed to apply AI proposal", err, { + operation: "ai_proposal_apply_failed", + userId, + }); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "ai_proposal_applied", + resourceType: "ai_proposal", + resourceId: String(id), + ipAddress, + userAgent, + success: false, + errorMessage: message, + }); + + res.status(400).json({ error: message }); + } + }, +); + +/** + * @openapi + * /ai/proposals/{id}/reject: + * post: + * summary: Reject a pending proposal + * tags: + * - AI + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Proposal rejected. + * 404: + * description: Proposal not found. + */ +router.post( + "/proposals/:id/reject", + authenticateJWT, + requireDataAccess, + aiGate, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId as string; + const id = parseId(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid proposal id" }); + + try { + const updated = await createCurrentAiRepository().setProposalStatus( + id, + userId, + "rejected", + ); + if (!updated) { + return res + .status(404) + .json({ error: "Proposal not found or already resolved" }); + } + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "ai_proposal_rejected", + resourceType: "ai_proposal", + resourceId: String(id), + ipAddress, + userAgent, + success: true, + }); + + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to reject AI proposal", err, { + operation: "ai_proposal_reject_failed", + userId, + }); + res.status(500).json({ error: "Failed to reject the proposal" }); + } + }, +); + +export default router; diff --git a/src/backend/ai/providers/anthropic.ts b/src/backend/ai/providers/anthropic.ts new file mode 100644 index 0000000..d07f2ad --- /dev/null +++ b/src/backend/ai/providers/anthropic.ts @@ -0,0 +1,162 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { providerFetch } from "./http.js"; +import type { + ChatChunk, + ChatRequest, + ProviderAdapter, + ProviderConfig, +} from "./types.js"; +import { AiProviderError } from "./types.js"; + +/** + * Model ids offered in the picker. Users can type any other id; this is a + * convenience list, not a restriction. + */ +export const ANTHROPIC_MODELS = [ + "claude-opus-5", + "claude-sonnet-5", + "claude-haiku-4-5", +]; + +function createClient(config: ProviderConfig): Anthropic { + if (!config.apiKey) { + throw new AiProviderError("This provider needs an API key"); + } + return new Anthropic({ + apiKey: config.apiKey, + ...(config.baseUrl?.trim() ? { baseURL: config.baseUrl.trim() } : {}), + // Routes the SDK's HTTP through the shared egress guard. + fetch: providerFetch as unknown as typeof fetch, + }); +} + +function toAnthropicMessages(request: ChatRequest): Anthropic.MessageParam[] { + const messages: Anthropic.MessageParam[] = []; + + for (const message of request.messages) { + if (message.role === "tool") { + messages.push({ + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: message.toolCallId ?? "", + content: message.content, + }, + ], + }); + continue; + } + + if (message.role === "assistant" && message.toolCalls?.length) { + const content: Anthropic.ContentBlockParam[] = []; + if (message.content) + content.push({ type: "text", text: message.content }); + for (const call of message.toolCalls) { + content.push({ + type: "tool_use", + id: call.id, + name: call.name, + input: call.arguments, + }); + } + messages.push({ role: "assistant", content }); + continue; + } + + if (message.role === "system") continue; + messages.push({ role: message.role, content: message.content }); + } + + return messages; +} + +/** + * The SDK throws its own typed errors rather than going through assertOk, so + * they are translated here to match what every other provider reports. + */ +function translateSdkError(error: unknown): never { + const status = + typeof (error as { status?: unknown })?.status === "number" + ? (error as { status: number }).status + : undefined; + const detail = error instanceof Error ? error.message : String(error); + + if (status === 429) { + throw new AiProviderError( + `Anthropic rate limit reached. Wait a moment and try again, or check your plan's quota. (${detail})`, + 429, + ); + } + if (status === 401 || status === 403) { + throw new AiProviderError( + `Anthropic rejected the API key. Check that it is correct and still active. (${detail})`, + status, + ); + } + throw new AiProviderError( + status ? `Anthropic request failed (${status}): ${detail}` : detail, + status, + ); +} + +export const anthropicAdapter: ProviderAdapter = { + async *streamChat( + config: ProviderConfig, + request: ChatRequest, + ): AsyncIterable { + const client = createClient(config); + + const stream = client.messages.stream({ + model: request.model, + max_tokens: 16000, + system: request.system, + messages: toAnthropicMessages(request), + thinking: { type: "adaptive" }, + ...(request.tools.length + ? { + tools: request.tools.map((tool) => ({ + name: tool.name, + description: tool.description, + input_schema: tool.parameters as Anthropic.Tool.InputSchema, + })), + } + : {}), + ...(request.signal ? { signal: request.signal } : {}), + }); + + let final: Anthropic.Message; + try { + for await (const event of stream) { + if ( + event.type === "content_block_delta" && + event.delta.type === "text_delta" + ) { + yield { type: "text", text: event.delta.text }; + } + } + final = await stream.finalMessage(); + } catch (error) { + translateSdkError(error); + } + + for (const block of final.content) { + if (block.type === "tool_use") { + yield { + type: "tool_call", + call: { + id: block.id, + name: block.name, + arguments: (block.input ?? {}) as Record, + }, + }; + } + } + + yield { type: "done", stopReason: final.stop_reason ?? undefined }; + }, + + async listModels(): Promise { + return [...ANTHROPIC_MODELS]; + }, +}; diff --git a/src/backend/ai/providers/gemini.ts b/src/backend/ai/providers/gemini.ts new file mode 100644 index 0000000..ff974f7 --- /dev/null +++ b/src/backend/ai/providers/gemini.ts @@ -0,0 +1,191 @@ +import { assertOk, providerFetch, readSseLines } from "./http.js"; +import type { + ChatChunk, + ChatRequest, + ProviderAdapter, + ProviderConfig, +} from "./types.js"; +import { AiProviderError } from "./types.js"; + +const GEMINI_DEFAULT_BASE = "https://generativelanguage.googleapis.com/v1beta"; + +function baseFor(config: ProviderConfig): string { + return config.baseUrl?.trim() || GEMINI_DEFAULT_BASE; +} + +/** + * Gemini has no tool role: a tool result is a user-side functionResponse part, + * and an assistant tool request is a model-side functionCall part. + */ +function toGeminiContents(request: ChatRequest): unknown[] { + const contents: unknown[] = []; + + for (const message of request.messages) { + if (message.role === "tool") { + contents.push({ + role: "user", + parts: [ + { + functionResponse: { + name: message.toolName ?? "tool", + response: { result: message.content }, + }, + }, + ], + }); + continue; + } + + if (message.role === "assistant" && message.toolCalls?.length) { + const parts: unknown[] = []; + if (message.content) parts.push({ text: message.content }); + for (const call of message.toolCalls) { + // thoughtSignature must be returned exactly as received or Gemini + // 2.5+ rejects the turn with a 400. + parts.push({ + functionCall: { name: call.name, args: call.arguments }, + ...(call.providerSignature + ? { thoughtSignature: call.providerSignature } + : {}), + }); + } + contents.push({ role: "model", parts }); + continue; + } + + if (message.role === "system") continue; + + contents.push({ + role: message.role === "assistant" ? "model" : "user", + parts: [{ text: message.content }], + }); + } + + return contents; +} + +/** + * Gemini rejects the JSON Schema keywords it does not implement, so the tool + * schemas are trimmed to the subset it accepts. + */ +function toGeminiSchema(schema: unknown): unknown { + if (!schema || typeof schema !== "object") return schema; + if (Array.isArray(schema)) return schema.map(toGeminiSchema); + + const source = schema as Record; + const output: Record = {}; + + for (const [key, value] of Object.entries(source)) { + if (key === "additionalProperties" || key === "$schema") continue; + if (key === "properties" && value && typeof value === "object") { + const properties: Record = {}; + for (const [name, child] of Object.entries( + value as Record, + )) { + properties[name] = toGeminiSchema(child); + } + output[key] = properties; + continue; + } + output[key] = toGeminiSchema(value); + } + + return output; +} + +export const geminiAdapter: ProviderAdapter = { + async *streamChat( + config: ProviderConfig, + request: ChatRequest, + ): AsyncIterable { + if (!config.apiKey) { + throw new AiProviderError("This provider needs an API key"); + } + + const url = `${baseFor(config).replace(/\/+$/, "")}/models/${encodeURIComponent(request.model)}:streamGenerateContent?alt=sse`; + + const response = await providerFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": config.apiKey, + }, + signal: request.signal, + body: JSON.stringify({ + systemInstruction: { parts: [{ text: request.system }] }, + contents: toGeminiContents(request), + ...(request.tools.length + ? { + tools: [ + { + functionDeclarations: request.tools.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: toGeminiSchema(tool.parameters), + })), + }, + ], + } + : {}), + }), + }); + + await assertOk(response, "Gemini"); + + let index = 0; + let stopReason: string | undefined; + + for await (const data of readSseLines(response)) { + let frame: any; + try { + frame = JSON.parse(data); + } catch { + continue; + } + + const candidate = frame.candidates?.[0]; + if (!candidate) continue; + if (candidate.finishReason) stopReason = candidate.finishReason; + + for (const part of candidate.content?.parts ?? []) { + if (typeof part.text === "string" && part.text) { + yield { type: "text", text: part.text }; + } + if (part.functionCall?.name) { + yield { + type: "tool_call", + call: { + id: `call_${part.functionCall.name}_${index++}`, + name: part.functionCall.name, + arguments: (part.functionCall.args ?? {}) as Record< + string, + unknown + >, + // Carried so the next turn can echo it back; without it Gemini + // 400s as soon as a tool has been used once. + providerSignature: part.thoughtSignature, + }, + }; + } + } + } + + yield { type: "done", stopReason }; + }, + + async listModels(config: ProviderConfig): Promise { + if (!config.apiKey) return []; + + const response = await providerFetch( + `${baseFor(config).replace(/\/+$/, "")}/models`, + { method: "GET", headers: { "x-goog-api-key": config.apiKey } }, + ); + await assertOk(response, "Gemini"); + + const body: any = await response.json(); + return (body.models ?? []) + .map((entry: any) => String(entry.name ?? "").replace(/^models\//, "")) + .filter((name: string) => name.length > 0) + .sort(); + }, +}; diff --git a/src/backend/ai/providers/http.ts b/src/backend/ai/providers/http.ts new file mode 100644 index 0000000..6195da8 --- /dev/null +++ b/src/backend/ai/providers/http.ts @@ -0,0 +1,177 @@ +import { getFetchDispatcher } from "../../utils/proxy-agent.js"; +import { safeOutboundFetch } from "../../utils/safe-outbound-fetch.js"; +import { evaluateEgress, readPrivateAllowlist } from "../egress.js"; +import { AiProviderError } from "./types.js"; + +/** + * Every outbound provider request goes through here so the egress rules cannot + * be bypassed by an adapter calling fetch directly. + * + * Public hosts use safeOutboundFetch, which re-checks the resolved address at + * connect time. Allowlisted private hosts cannot use it (its whole job is to + * refuse them), so they fall back to plain fetch with the proxy dispatcher -- + * still respecting corporate proxy configuration. + */ +export async function providerFetch( + url: string, + init: RequestInit, +): Promise { + const allowlist = await readPrivateAllowlist(); + const decision = evaluateEgress(url, allowlist); + + if (!decision.allowed) { + throw new AiProviderError(decision.reason ?? "Destination not allowed"); + } + + if (decision.isPrivate) { + return fetch(url, { + ...init, + dispatcher: getFetchDispatcher(url), + } as RequestInit); + } + + return safeOutboundFetch(url, init) as unknown as Promise; +} + +export function joinUrl(base: string, path: string): string { + return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`; +} + +/** + * Yields the data payload of each SSE frame. Providers differ in what they put + * inside, so parsing the JSON is left to the caller. + */ +export async function* readSseLines( + response: Response, +): AsyncGenerator { + const body = response.body; + if (!body) return; + + const reader = body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex !== -1) { + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + if (line.startsWith("data:")) { + yield line.slice(5).trim(); + } + newlineIndex = buffer.indexOf("\n"); + } + } + } finally { + reader.releaseLock(); + } +} + +/** Yields one parsed JSON object per line, for newline-delimited streams. */ +export async function* readJsonLines( + response: Response, +): AsyncGenerator { + const body = response.body; + if (!body) return; + + const reader = body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex !== -1) { + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + if (line) { + try { + yield JSON.parse(line); + } catch { + // A partial or malformed frame is skipped rather than failing the + // whole stream. + } + } + newlineIndex = buffer.indexOf("\n"); + } + } + } finally { + reader.releaseLock(); + } +} + +/** + * Digs the human-readable message out of an error body. + * + * Every provider nests it differently, and dumping the raw JSON produced + * something that got cut off mid-sentence. Falls back to a trimmed snippet + * when the shape is unfamiliar. + */ +function extractProviderMessage(body: string): string { + try { + const parsed = JSON.parse(body); + const message = + parsed?.error?.message ?? + parsed?.error?.["message"] ?? + parsed?.message ?? + (typeof parsed?.error === "string" ? parsed.error : null); + if (typeof message === "string" && message.trim()) { + return message.trim(); + } + } catch { + // Not JSON; fall through to the snippet. + } + + const trimmed = body.trim(); + if (!trimmed) return ""; + return trimmed.length > 300 ? `${trimmed.slice(0, 300)}...` : trimmed; +} + +export async function assertOk( + response: Response, + provider: string, +): Promise { + if (response.ok) return; + + let body = ""; + try { + body = await response.text(); + } catch { + body = ""; + } + + const detail = extractProviderMessage(body); + + // Rate limits and auth failures are the two the user can actually act on, + // so they say what to do instead of reading like an internal failure. + if (response.status === 429) { + throw new AiProviderError( + `${provider} rate limit reached. Wait a moment and try again, or check your plan's quota.${ + detail ? ` (${detail})` : "" + }`, + 429, + ); + } + if (response.status === 401 || response.status === 403) { + throw new AiProviderError( + `${provider} rejected the API key. Check that it is correct and still active.${ + detail ? ` (${detail})` : "" + }`, + response.status, + ); + } + + throw new AiProviderError( + `${provider} request failed (${response.status})${detail ? `: ${detail}` : ""}`, + response.status, + ); +} diff --git a/src/backend/ai/providers/ollama.ts b/src/backend/ai/providers/ollama.ts new file mode 100644 index 0000000..622b69c --- /dev/null +++ b/src/backend/ai/providers/ollama.ts @@ -0,0 +1,131 @@ +import { assertOk, joinUrl, providerFetch, readJsonLines } from "./http.js"; +import type { + ChatChunk, + ChatRequest, + ProviderAdapter, + ProviderConfig, +} from "./types.js"; + +export const OLLAMA_DEFAULT_BASE = "http://localhost:11434"; + +function baseFor(config: ProviderConfig): string { + return config.baseUrl?.trim() || OLLAMA_DEFAULT_BASE; +} + +function toOllamaMessages(request: ChatRequest): unknown[] { + const messages: unknown[] = [{ role: "system", content: request.system }]; + + for (const message of request.messages) { + if (message.role === "tool") { + messages.push({ + role: "tool", + content: message.content, + ...(message.toolName ? { tool_name: message.toolName } : {}), + }); + continue; + } + + if (message.role === "assistant" && message.toolCalls?.length) { + messages.push({ + role: "assistant", + content: message.content, + tool_calls: message.toolCalls.map((call) => ({ + function: { name: call.name, arguments: call.arguments }, + })), + }); + continue; + } + + messages.push({ role: message.role, content: message.content }); + } + + return messages; +} + +export const ollamaAdapter: ProviderAdapter = { + async *streamChat( + config: ProviderConfig, + request: ChatRequest, + ): AsyncIterable { + const response = await providerFetch(joinUrl(baseFor(config), "api/chat"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + signal: request.signal, + body: JSON.stringify({ + model: request.model, + messages: toOllamaMessages(request), + stream: true, + ...(request.tools.length + ? { + tools: request.tools.map((tool) => ({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + }, + })), + } + : {}), + }), + }); + + await assertOk(response, "Ollama"); + + let index = 0; + let stopReason: string | undefined; + + // Ollama streams newline-delimited JSON rather than SSE. + for await (const frame of readJsonLines(response)) { + const payload = frame as any; + + if ( + typeof payload.message?.content === "string" && + payload.message.content + ) { + yield { type: "text", text: payload.message.content }; + } + + for (const call of payload.message?.tool_calls ?? []) { + const name = call.function?.name; + if (!name) continue; + const rawArgs = call.function?.arguments; + // Ollama sends an object, but some builds send a JSON string. + let args: Record = {}; + if (rawArgs && typeof rawArgs === "object") { + args = rawArgs as Record; + } else if (typeof rawArgs === "string" && rawArgs.trim()) { + try { + args = JSON.parse(rawArgs); + } catch { + args = {}; + } + } + yield { + type: "tool_call", + call: { id: `call_${name}_${index++}`, name, arguments: args }, + }; + } + + if (payload.done) { + stopReason = payload.done_reason ?? "stop"; + break; + } + } + + yield { type: "done", stopReason }; + }, + + async listModels(config: ProviderConfig): Promise { + const response = await providerFetch(joinUrl(baseFor(config), "api/tags"), { + method: "GET", + }); + await assertOk(response, "Ollama"); + + const body: any = await response.json(); + return (body.models ?? []) + .map((entry: any) => entry.name) + .filter((name: unknown): name is string => typeof name === "string") + .sort(); + }, +}; diff --git a/src/backend/ai/providers/openai.ts b/src/backend/ai/providers/openai.ts new file mode 100644 index 0000000..8ba3e53 --- /dev/null +++ b/src/backend/ai/providers/openai.ts @@ -0,0 +1,182 @@ +import { assertOk, joinUrl, providerFetch, readSseLines } from "./http.js"; +import type { + ChatChunk, + ChatRequest, + ProviderAdapter, + ProviderConfig, + ToolCall, +} from "./types.js"; +import { AiProviderError } from "./types.js"; + +const OPENAI_DEFAULT_BASE = "https://api.openai.com/v1"; + +function baseFor(config: ProviderConfig): string { + if (config.baseUrl?.trim()) return config.baseUrl.trim(); + if (config.providerType === "openai") return OPENAI_DEFAULT_BASE; + throw new AiProviderError("This provider needs a base URL"); +} + +function toOpenAiMessages(request: ChatRequest): unknown[] { + const messages: unknown[] = [{ role: "system", content: request.system }]; + + for (const message of request.messages) { + if (message.role === "tool") { + messages.push({ + role: "tool", + tool_call_id: message.toolCallId, + content: message.content, + }); + continue; + } + + if (message.role === "assistant" && message.toolCalls?.length) { + messages.push({ + role: "assistant", + content: message.content || null, + tool_calls: message.toolCalls.map((call) => ({ + id: call.id, + type: "function", + function: { + name: call.name, + arguments: JSON.stringify(call.arguments), + }, + })), + }); + continue; + } + + messages.push({ role: message.role, content: message.content }); + } + + return messages; +} + +/** + * Tool call arguments arrive as JSON fragments spread across many deltas, so + * they are accumulated per index and only parsed once the stream ends. + */ +interface PartialCall { + id: string; + name: string; + args: string; +} + +export const openAiAdapter: ProviderAdapter = { + async *streamChat( + config: ProviderConfig, + request: ChatRequest, + ): AsyncIterable { + const url = joinUrl(baseFor(config), "chat/completions"); + + const headers: Record = { + "Content-Type": "application/json", + }; + if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`; + + const response = await providerFetch(url, { + method: "POST", + headers, + signal: request.signal, + body: JSON.stringify({ + model: request.model, + messages: toOpenAiMessages(request), + stream: true, + ...(request.tools.length + ? { + tools: request.tools.map((tool) => ({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + }, + })), + } + : {}), + }), + }); + + await assertOk(response, "OpenAI"); + + const partial = new Map(); + let stopReason: string | undefined; + + for await (const data of readSseLines(response)) { + if (data === "[DONE]") break; + + let frame: any; + try { + frame = JSON.parse(data); + } catch { + continue; + } + + const choice = frame.choices?.[0]; + if (!choice) continue; + + if (choice.finish_reason) stopReason = choice.finish_reason; + + const delta = choice.delta; + if (!delta) continue; + + if (typeof delta.content === "string" && delta.content) { + yield { type: "text", text: delta.content }; + } + + for (const call of delta.tool_calls ?? []) { + const index = call.index ?? 0; + const existing = partial.get(index) ?? { id: "", name: "", args: "" }; + if (call.id) existing.id = call.id; + if (call.function?.name) existing.name = call.function.name; + if (call.function?.arguments) existing.args += call.function.arguments; + partial.set(index, existing); + } + } + + for (const call of partial.values()) { + if (!call.name) continue; + yield { type: "tool_call", call: finalizeCall(call) }; + } + + yield { type: "done", stopReason }; + }, + + async listModels(config: ProviderConfig): Promise { + const headers: Record = {}; + if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`; + + const response = await providerFetch(joinUrl(baseFor(config), "models"), { + method: "GET", + headers, + }); + await assertOk(response, "OpenAI"); + + const body: any = await response.json(); + return (body.data ?? []) + .map((entry: any) => entry.id) + .filter((id: unknown): id is string => typeof id === "string") + .sort(); + }, +}; + +export function finalizeCall(call: PartialCall): ToolCall { + let args: Record = {}; + if (call.args.trim()) { + try { + const parsed = JSON.parse(call.args); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + args = parsed as Record; + } + } catch { + // A model that emitted malformed arguments gets an empty object; the + // tool's own schema validation reports the problem back to it. + args = {}; + } + } + return { + id: + call.id || `call_${call.name}_${Math.random().toString(36).slice(2, 10)}`, + name: call.name, + arguments: args, + }; +} diff --git a/src/backend/ai/providers/registry.ts b/src/backend/ai/providers/registry.ts new file mode 100644 index 0000000..0bbc987 --- /dev/null +++ b/src/backend/ai/providers/registry.ts @@ -0,0 +1,64 @@ +import { anthropicAdapter } from "./anthropic.js"; +import { geminiAdapter } from "./gemini.js"; +import { ollamaAdapter } from "./ollama.js"; +import { openAiAdapter } from "./openai.js"; +import { + AiProviderError, + type AiProviderType, + type ProviderAdapter, +} from "./types.js"; + +/** + * openai_compatible reuses the OpenAI adapter: OpenRouter, Groq, Mistral, + * DeepSeek, together.ai, LM Studio and vLLM all speak the same wire format, + * they differ only in base URL. + */ +const ADAPTERS: Record = { + ollama: ollamaAdapter, + anthropic: anthropicAdapter, + openai: openAiAdapter, + gemini: geminiAdapter, + openai_compatible: openAiAdapter, +}; + +export function getAdapter(providerType: string): ProviderAdapter { + const adapter = ADAPTERS[providerType as AiProviderType]; + if (!adapter) { + throw new AiProviderError(`Unknown provider type: ${providerType}`); + } + return adapter; +} + +/** Provider types that cannot work without a base URL. */ +export const REQUIRES_BASE_URL: AiProviderType[] = [ + "ollama", + "openai_compatible", +]; + +/** Provider types that cannot work without an API key. */ +export const REQUIRES_API_KEY: AiProviderType[] = [ + "anthropic", + "openai", + "gemini", +]; + +/** + * Shown when a provider's model list cannot be fetched: no key entered yet, + * the endpoint is unreachable, or the vendor has no list endpoint. Users can + * always type a model id the list does not contain, so this is a starting + * point rather than a restriction. + */ +export const FALLBACK_MODELS: Record = { + ollama: [ + "llama3.2", + "llama3.1", + "qwen2.5-coder", + "mistral", + "phi4", + "gemma2", + ], + anthropic: ["claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5"], + openai: ["gpt-5", "gpt-5-mini", "o4-mini", "gpt-4.1"], + gemini: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"], + openai_compatible: [], +}; diff --git a/src/backend/ai/providers/types.ts b/src/backend/ai/providers/types.ts new file mode 100644 index 0000000..e170dfb --- /dev/null +++ b/src/backend/ai/providers/types.ts @@ -0,0 +1,90 @@ +/** + * The shape every provider adapter normalizes to. Adding a provider means + * translating its wire format into these events; nothing downstream (the + * engine, the tool dispatcher, the SSE route) knows which vendor is in use. + */ + +export type AiProviderType = + "ollama" | "anthropic" | "openai" | "gemini" | "openai_compatible"; + +export const AI_PROVIDER_TYPES: AiProviderType[] = [ + "ollama", + "anthropic", + "openai", + "gemini", + "openai_compatible", +]; + +export function isAiProviderType(value: unknown): value is AiProviderType { + return ( + typeof value === "string" && (AI_PROVIDER_TYPES as string[]).includes(value) + ); +} + +export interface ChatMessage { + role: "system" | "user" | "assistant" | "tool"; + content: string; + /** Set on assistant turns that requested tools. */ + toolCalls?: ToolCall[]; + /** Set on tool turns, matching the id of the call being answered. */ + toolCallId?: string; + toolName?: string; +} + +export interface ToolCall { + id: string; + name: string; + arguments: Record; + /** + * Opaque provider state that has to be echoed back verbatim on the next + * turn. Gemini 2.5+ rejects a follow-up whose functionCall parts have lost + * their thoughtSignature, so this rides along rather than being dropped. + */ + providerSignature?: string; +} + +export interface ToolDefinition { + name: string; + description: string; + parameters: Record; +} + +export interface ChatRequest { + model: string; + system: string; + messages: ChatMessage[]; + tools: ToolDefinition[]; + signal?: AbortSignal; +} + +export type ChatChunk = + | { type: "text"; text: string } + | { type: "tool_call"; call: ToolCall } + | { type: "done"; stopReason?: string } + | { type: "error"; message: string }; + +export interface ProviderConfig { + providerType: AiProviderType; + baseUrl?: string | null; + apiKey?: string | null; +} + +export interface ProviderAdapter { + /** Streams a single assistant turn. Tool execution happens in the engine. */ + streamChat( + config: ProviderConfig, + request: ChatRequest, + ): AsyncIterable; + /** Model ids to offer in the picker, best effort. */ + listModels(config: ProviderConfig): Promise; +} + +export class AiProviderError extends Error { + constructor( + message: string, + readonly status?: number, + ) { + super(message); + this.name = "AiProviderError"; + } +} diff --git a/src/backend/ai/redaction.ts b/src/backend/ai/redaction.ts new file mode 100644 index 0000000..4876182 --- /dev/null +++ b/src/backend/ai/redaction.ts @@ -0,0 +1,90 @@ +/** + * Defense in depth for anything about to leave the server. + * + * Read tools already select explicit field allowlists rather than spreading + * rows, so nothing secret should reach here. This exists because "should" is + * not a guarantee: a future tool that forgets to project its fields would + * otherwise ship credentials to a third-party model provider. + */ + +const SECRET_KEY_PATTERN = + /^(password|passwd|pass|secret|token|api_?key|apikey|private_?key|privatekey|key_?password|keypassword|passphrase|client_?secret|authorization|auth_?token|access_?token|refresh_?token|totp_?secret|backup_?codes|session_?token|cookie|credential|ssh_?cert|data_?key|dek)$/i; + +/** Substring markers for keys that are not exact matches but still sensitive. */ +const SECRET_KEY_SUBSTRINGS = [ + "password", + "secret", + "privatekey", + "private_key", + "apikey", + "api_key", + "passphrase", +]; + +const VALUE_PATTERNS: Array<{ pattern: RegExp; label: string }> = [ + { + pattern: + /-----BEGIN[^-]*PRIVATE KEY-----[\s\S]*?-----END[^-]*PRIVATE KEY-----/g, + label: "[redacted private key]", + }, + { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, label: "[redacted api key]" }, + { pattern: /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g, label: "[redacted api key]" }, + { pattern: /\bghp_[A-Za-z0-9]{20,}\b/g, label: "[redacted token]" }, + { pattern: /\btmx_[A-Za-z0-9_-]{16,}\b/g, label: "[redacted token]" }, + { + pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+\b/g, + label: "[redacted token]", + }, + { + pattern: /\bBearer\s+[A-Za-z0-9._-]{16,}/gi, + label: "Bearer [redacted]", + }, +]; + +export const REDACTED = "[redacted]"; + +function isSecretKey(key: string): boolean { + if (SECRET_KEY_PATTERN.test(key)) return true; + const lower = key.toLowerCase(); + return SECRET_KEY_SUBSTRINGS.some((marker) => lower.includes(marker)); +} + +export function redactString(value: string): string { + let output = value; + for (const { pattern, label } of VALUE_PATTERNS) { + output = output.replace(pattern, label); + } + return output; +} + +/** + * Recursively drops secret-named fields and masks secret-shaped values. + * Depth is bounded so a cyclic or pathological structure cannot hang the loop. + */ +export function redact(value: unknown, depth = 0): unknown { + if (depth > 12) return REDACTED; + + if (typeof value === "string") return redactString(value); + if (value === null || typeof value !== "object") return value; + + if (Array.isArray(value)) { + return value.map((entry) => redact(entry, depth + 1)); + } + + const output: Record = {}; + for (const [key, entry] of Object.entries(value as Record)) { + if (isSecretKey(key)) { + // Preserve the shape so the model can still reason about presence, + // without ever seeing the value. + output[key] = entry === null || entry === undefined ? null : REDACTED; + continue; + } + output[key] = redact(entry, depth + 1); + } + return output; +} + +/** Convenience wrapper for serializing a tool result. */ +export function redactToJson(value: unknown): string { + return JSON.stringify(redact(value)); +} diff --git a/src/backend/ai/tools/catalog.ts b/src/backend/ai/tools/catalog.ts new file mode 100644 index 0000000..2edbf55 --- /dev/null +++ b/src/backend/ai/tools/catalog.ts @@ -0,0 +1,68 @@ +import { proposeTools } from "./propose-tools.js"; +import { readTools } from "./read-tools.js"; +import type { AiTool, ToolDefinitionShape } from "./types.js"; + +/** + * The allowlist, and the security boundary for the whole feature. + * + * A model can only ever invoke what appears here. This matters more than usual + * in this codebase: PermissionManager.requirePermission exists but is currently + * mounted on zero routes, so RBAC strings are a vocabulary for the admin role + * editor rather than route enforcement. "The assistant cannot reach credentials + * or user administration" is therefore a property of this list, not of the + * permission system. + * + * Anything touching credentials, vaults, RBAC, users, identity, certificates, + * SSO or instance settings is deliberately absent and must stay absent. + */ +export const AI_TOOLS: AiTool[] = [...readTools, ...proposeTools]; + +const BY_NAME = new Map(AI_TOOLS.map((tool) => [tool.name, tool])); + +export function getTool(name: string): AiTool | undefined { + return BY_NAME.get(name); +} + +export function listToolNames(): string[] { + return AI_TOOLS.map((tool) => tool.name); +} + +/** + * Domains the assistant must never be able to reach, in any tool, ever. + * The catalog test asserts no tool name references these. + */ +export const FORBIDDEN_DOMAINS = [ + "credential", + "vault", + "rbac", + "role", + "permission", + "user_admin", + "password", + "totp", + "webauthn", + "passkey", + "api_key", + "session", + "oidc", + "sso", + "ldap", + "termix_id", + "identity", + "certificate", + "opkssh", + "acme", + "ssl", + "audit", + "sync", + "settings", +]; + +/** Tool definitions in the shape the provider adapters expect. */ +export function toolDefinitions(): ToolDefinitionShape[] { + return AI_TOOLS.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: tool.parameters, + })); +} diff --git a/src/backend/ai/tools/command-allowlist.ts b/src/backend/ai/tools/command-allowlist.ts new file mode 100644 index 0000000..16b15c6 --- /dev/null +++ b/src/backend/ai/tools/command-allowlist.ts @@ -0,0 +1,136 @@ +/** + * Which commands the assistant may run without a per-command approval click, + * for users who opted into read-only execution. + * + * The check parses the command into arguments and matches the resolved binary + * against the allowlist. Substring matching would be trivially defeated + * ("df; rm -rf /" contains "df"), so any shell metacharacter that could chain, + * redirect or substitute a second command rejects the whole string outright. + */ + +export const READ_ONLY_COMMANDS = new Set([ + "df", + "du", + "free", + "uptime", + "uname", + "whoami", + "hostname", + "id", + "ps", + "top", + "systemctl", + "journalctl", + "docker", + "ip", + "ss", + "netstat", + "lsblk", + "cat", + "ls", + "stat", + "which", + "date", + "lscpu", + "vmstat", + "iostat", +]); + +/** Characters that let one command become several. */ +const SHELL_METACHARACTERS = /[;&|`$><\n\r\\]/; + +/** Subcommands that are safe for otherwise-powerful binaries. */ +const SUBCOMMAND_ALLOWLIST: Record> = { + systemctl: new Set([ + "status", + "list-units", + "list-unit-files", + "is-active", + "is-enabled", + "show", + ]), + docker: new Set([ + "ps", + "stats", + "images", + "logs", + "inspect", + "version", + "info", + ]), + ip: new Set(["a", "addr", "link", "route", "neigh"]), +}; + +/** Paths `cat` may read. Anything else could disclose credentials. */ +const CAT_ALLOWED_PREFIXES = ["/proc/", "/sys/", "/etc/os-release"]; + +export interface CommandCheck { + allowed: boolean; + reason?: string; +} + +export function isReadOnlyCommand(raw: string): CommandCheck { + const command = raw.trim(); + if (!command) return { allowed: false, reason: "Empty command" }; + + if (SHELL_METACHARACTERS.test(command)) { + return { + allowed: false, + reason: "Command chaining, redirection and substitution are not allowed", + }; + } + + const parts = command.split(/\s+/).filter(Boolean); + if (!parts.length) return { allowed: false, reason: "Empty command" }; + + // Reject env-prefixed and privilege-escalating forms outright. + const head = parts[0]; + if (head === "sudo" || head === "su" || head === "doas" || head === "env") { + return { allowed: false, reason: `${head} is not allowed` }; + } + + // A path like /usr/bin/df resolves to its basename. + const binary = head.includes("/") + ? head.slice(head.lastIndexOf("/") + 1) + : head; + + if (!READ_ONLY_COMMANDS.has(binary)) { + return { + allowed: false, + reason: `${binary} is not on the read-only allowlist`, + }; + } + + const allowedSubcommands = SUBCOMMAND_ALLOWLIST[binary]; + if (allowedSubcommands) { + const subcommand = parts.slice(1).find((part) => !part.startsWith("-")); + if (!subcommand || !allowedSubcommands.has(subcommand)) { + return { + allowed: false, + reason: + `${binary} ${subcommand ?? ""}`.trim() + + " is not on the read-only allowlist", + }; + } + } + + if (binary === "cat") { + const targets = parts.slice(1).filter((part) => !part.startsWith("-")); + if (!targets.length) { + return { allowed: false, reason: "cat needs a file path" }; + } + for (const target of targets) { + const permitted = CAT_ALLOWED_PREFIXES.some((prefix) => + prefix.endsWith("/") ? target.startsWith(prefix) : target === prefix, + ); + if (!permitted) { + return { + allowed: false, + reason: `cat is limited to ${CAT_ALLOWED_PREFIXES.join(", ")}`, + }; + } + } + } + + return { allowed: true }; +} diff --git a/src/backend/ai/tools/executor.ts b/src/backend/ai/tools/executor.ts new file mode 100644 index 0000000..1459e0e --- /dev/null +++ b/src/backend/ai/tools/executor.ts @@ -0,0 +1,332 @@ +import { + createCurrentAlertRepository, + createCurrentAutomationRepository, + createCurrentFleetRepository, + createCurrentHostRepository, + createCurrentSnippetRepository, +} from "../../database/repositories/factory.js"; +import { validateDefinition } from "../../database/routes/automations.js"; +import { resolveHostById } from "../../hosts/host-resolver.js"; +import { execCommand } from "../../hosts/metrics/widgets/common-utils.js"; +import { + createFleetSshFactory, + getFleetPoolKey, +} from "../../hosts/ssh-client-factory.js"; +import { withConnection } from "../../hosts/ssh-connection-pool.js"; +import { getTool } from "./catalog.js"; + +/** Approved commands get a bounded window rather than hanging the request. */ +const COMMAND_TIMEOUT_MS = 60_000; + +/** + * Applies an approved proposal. + * + * The stored payload is treated as untrusted input even though the server wrote + * it: the proposal could have sat in the table across a release, and defending + * the apply path rather than the create path means one place to get right. + * Everything goes through the same repositories a human action uses, scoped to + * the approving user. + */ + +export interface ApplyResult { + ok: boolean; + summary: string; +} + +function requireNumber(value: unknown, field: string): number { + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`${field} must be a positive integer`); + } + return parsed; +} + +function requireString(value: unknown, field: string): string { + if (typeof value !== "string" || !value.trim()) { + throw new Error(`${field} is required`); + } + return value.trim(); +} + +function optionalString(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} + +export async function applyProposal( + kind: string, + payload: Record, + userId: string, +): Promise { + // A payload whose tool no longer exists is refused rather than guessed at. + if (!getTool(kind)) { + throw new Error(`Unknown proposal kind: ${kind}`); + } + + switch (kind) { + case "propose_create_host": { + const created = await createCurrentHostRepository().create({ + userId, + name: requireString(payload.name, "name"), + ip: requireString(payload.ip, "ip"), + port: Number(payload.port) || 22, + username: optionalString(payload.username) ?? "", + folder: optionalString(payload.folder) ?? "", + tags: JSON.stringify(Array.isArray(payload.tags) ? payload.tags : []), + } as any); + return { + ok: true, + summary: `Created host ${(created as any).name ?? ""}`.trim(), + }; + } + + case "propose_update_host": { + const hostId = requireNumber(payload.hostId, "hostId"); + const changes = (payload.changes ?? {}) as Record; + + const existing = await createCurrentHostRepository().findByIdForUser( + userId, + hostId, + ); + if (!existing) throw new Error("Host not found"); + + const updates: Record = {}; + if (changes.name !== undefined) + updates.name = requireString(changes.name, "name"); + if (changes.ip !== undefined) + updates.ip = requireString(changes.ip, "ip"); + if (changes.port !== undefined) updates.port = Number(changes.port); + if (changes.username !== undefined) + updates.username = optionalString(changes.username) ?? ""; + if (changes.folder !== undefined) + updates.folder = optionalString(changes.folder) ?? ""; + if (changes.tags !== undefined) { + updates.tags = JSON.stringify( + Array.isArray(changes.tags) ? changes.tags : [], + ); + } + + if (!Object.keys(updates).length) { + return { ok: false, summary: "Nothing to change" }; + } + + await createCurrentHostRepository().updateForUser( + userId, + hostId, + updates as any, + ); + return { ok: true, summary: `Updated host ${hostId}` }; + } + + case "propose_delete_host": { + const hostId = requireNumber(payload.hostId, "hostId"); + const deleted = await createCurrentHostRepository().deleteForUser( + userId, + hostId, + ); + if (!deleted) throw new Error("Host not found"); + return { ok: true, summary: `Deleted host ${hostId}` }; + } + + case "propose_create_snippet": { + const created = await createCurrentSnippetRepository().createSnippet( + userId, + { + name: requireString(payload.name, "name"), + content: requireString(payload.content, "content"), + description: optionalString(payload.description), + folder: optionalString(payload.folder), + } as any, + ); + return { + ok: true, + summary: `Created snippet ${(created as any)?.name ?? ""}`.trim(), + }; + } + + case "propose_update_snippet": { + const snippetId = requireNumber(payload.snippetId, "snippetId"); + const changes = (payload.changes ?? {}) as Record; + + const existing = await createCurrentSnippetRepository().findOwnedById( + userId, + snippetId, + ); + if (!existing) throw new Error("Snippet not found"); + + const updates: Record = {}; + if (changes.name !== undefined) + updates.name = requireString(changes.name, "name"); + if (changes.content !== undefined) + updates.content = requireString(changes.content, "content"); + if (changes.description !== undefined) + updates.description = optionalString(changes.description); + if (changes.folder !== undefined) + updates.folder = optionalString(changes.folder); + + if (!Object.keys(updates).length) { + return { ok: false, summary: "Nothing to change" }; + } + + await createCurrentSnippetRepository().updateSnippet( + userId, + snippetId, + updates as any, + ); + return { ok: true, summary: `Updated snippet ${snippetId}` }; + } + + case "propose_delete_snippet": { + const snippetId = requireNumber(payload.snippetId, "snippetId"); + const deleted = await createCurrentSnippetRepository().deleteSnippet( + userId, + snippetId, + ); + if (!deleted) throw new Error("Snippet not found"); + return { ok: true, summary: `Deleted snippet ${snippetId}` }; + } + + case "propose_create_fleet": { + const fleet = await createCurrentFleetRepository().create(userId, { + name: requireString(payload.name, "name"), + description: optionalString(payload.description), + } as any); + + const hostIds = Array.isArray(payload.hostIds) ? payload.hostIds : []; + let added = 0; + for (const raw of hostIds) { + const hostId = Number(raw); + if (!Number.isInteger(hostId) || hostId <= 0) continue; + // Only hosts the approving user owns can join their fleet. + const host = await createCurrentHostRepository().findByIdForUser( + userId, + hostId, + ); + if (!host) continue; + await createCurrentFleetRepository().addMember( + (fleet as any).id, + hostId, + ); + added += 1; + } + + return { + ok: true, + summary: `Created fleet ${(fleet as any).name} with ${added} host${added === 1 ? "" : "s"}`, + }; + } + + case "propose_create_alert_rule": { + const created = await createCurrentAlertRepository().createAlertRule({ + userId, + name: requireString(payload.name, "name"), + hostId: + payload.hostId === null || payload.hostId === undefined + ? null + : requireNumber(payload.hostId, "hostId"), + enabled: true, + triggerType: requireString(payload.triggerType, "triggerType"), + thresholdValue: + payload.thresholdValue === null || + payload.thresholdValue === undefined + ? null + : Number(payload.thresholdValue), + thresholdDurationSeconds: + payload.thresholdDurationSeconds === null || + payload.thresholdDurationSeconds === undefined + ? null + : Number(payload.thresholdDurationSeconds), + cooldownMinutes: Number(payload.cooldownMinutes) || 15, + channelIds: [], + } as any); + return { + ok: true, + summary: `Created alert rule ${(created as any)?.name ?? ""}`.trim(), + }; + } + + case "propose_create_automation": { + // Reuses the same validator the automations route runs, so an + // LLM-authored definition is held to exactly the human standard. + const validation = validateDefinition(payload.definition); + if (!validation.ok || !validation.definition) { + throw new Error( + validation.error ?? "The automation definition is invalid", + ); + } + + const created = await createCurrentAutomationRepository().create({ + userId, + name: requireString(payload.name, "name"), + description: optionalString(payload.description), + definition: JSON.stringify(validation.definition), + // Starts disabled: an automation the user has not watched run once + // should not begin firing against their servers on approval. + enabled: false, + }); + + return { + ok: true, + summary: `Created automation ${(created as any).name} (disabled until you enable it)`, + }; + } + + case "propose_run_command": { + const hostId = requireNumber(payload.hostId, "hostId"); + const command = requireString(payload.command, "command"); + + // resolveHostById, not the raw repository row: it runs the connect-level + // permission check, decrypts auth under the owner's key, and resolves the + // jump host chain. A plain row has none of that, so the SSH factory saw + // an unresolved jumpHosts field and failed the connection. + const host = await resolveHostById(hostId, userId); + if (!host) throw new Error("Host not found"); + + const result = await runCommandOnHost(host, command); + if (result.error) { + throw new Error(result.error); + } + return { + ok: true, + summary: result.output?.slice(0, 2000) ?? "(no output)", + }; + } + + default: + throw new Error(`Proposal kind ${kind} cannot be applied automatically`); + } +} + +/** + * Runs one approved command over the shared SSH pool, mirroring how an + * automation run_command step executes. + */ +async function runCommandOnHost( + host: Record, + command: string, +): Promise<{ output?: string; error?: string }> { + try { + const result = await withConnection( + getFleetPoolKey(host as any), + createFleetSshFactory(host as any), + async (client) => execCommand(client, command, COMMAND_TIMEOUT_MS), + ); + + const output = [result.stdout, result.stderr].filter(Boolean).join("\n"); + if (result.code === 0 || result.code === null) { + return { output: output || "(no output)" }; + } + return { error: `Exited with code ${result.code}: ${output}`.trim() }; + } catch (error) { + return { + error: error instanceof Error ? error.message : String(error), + }; + } +} + +/** + * Kinds the route handles itself rather than through applyProposal. + * Empty: everything the assistant can propose can now be applied. + */ +export const ROUTE_APPLIED_KINDS = new Set(); diff --git a/src/backend/ai/tools/propose-tools.ts b/src/backend/ai/tools/propose-tools.ts new file mode 100644 index 0000000..107d2aa --- /dev/null +++ b/src/backend/ai/tools/propose-tools.ts @@ -0,0 +1,279 @@ +import { num, objectSchema, proposal, str, type AiTool } from "./types.js"; + +/** + * Propose tools never mutate anything. They return a draft that is stored as a + * pending proposal and rendered as a card; the change happens only when the + * user approves it, and the payload is re-validated at that point. + */ + +export const proposeTools: AiTool[] = [ + { + name: "propose_create_host", + description: + "Propose adding a new SSH host. Never include credentials: the user attaches those themselves after approving.", + category: "propose", + parameters: objectSchema( + { + name: str("Display name for the host"), + ip: str("Hostname or IP address"), + port: num("SSH port (defaults to 22)"), + username: str("SSH username"), + folder: str("Folder to file the host under"), + tags: { + type: "array", + items: { type: "string" }, + description: "Tags to apply", + }, + }, + ["name", "ip"], + ), + handler: async (args) => + proposal("propose_create_host", `Add host ${String(args.name)}`, { + name: args.name, + ip: args.ip, + port: args.port ?? 22, + username: args.username ?? null, + folder: args.folder ?? null, + tags: args.tags ?? [], + }), + }, + { + name: "propose_update_host", + description: + "Propose changing an existing host's non-secret settings. Only include the fields that should change.", + category: "propose", + parameters: objectSchema( + { + hostId: num("The host id to update"), + name: str("New display name"), + ip: str("New hostname or IP address"), + port: num("New SSH port"), + username: str("New SSH username"), + folder: str("New folder"), + tags: { + type: "array", + items: { type: "string" }, + description: "Replacement tag list", + }, + }, + ["hostId"], + ), + handler: async (args) => { + const changes: Record = {}; + for (const field of [ + "name", + "ip", + "port", + "username", + "folder", + "tags", + ]) { + if (args[field] !== undefined) changes[field] = args[field]; + } + return proposal( + "propose_update_host", + `Update host ${String(args.hostId)}`, + { hostId: args.hostId, changes }, + ); + }, + }, + { + name: "propose_delete_host", + description: + "Propose removing a host. Use sparingly and explain why in the reason.", + category: "propose", + parameters: objectSchema( + { + hostId: num("The host id to delete"), + reason: str("Why this host should be removed"), + }, + ["hostId", "reason"], + ), + handler: async (args) => + proposal("propose_delete_host", `Delete host ${String(args.hostId)}`, { + hostId: args.hostId, + reason: args.reason, + }), + }, + { + name: "propose_create_snippet", + description: + "Propose saving a new command snippet the user can run against their hosts.", + category: "propose", + parameters: objectSchema( + { + name: str("Snippet name"), + content: str("The command text"), + description: str("What the snippet does"), + folder: str("Folder to file it under"), + }, + ["name", "content"], + ), + handler: async (args) => + proposal( + "propose_create_snippet", + `Create snippet ${String(args.name)}`, + { + name: args.name, + content: args.content, + description: args.description ?? null, + folder: args.folder ?? null, + }, + ), + }, + { + name: "propose_update_snippet", + description: "Propose editing an existing snippet.", + category: "propose", + parameters: objectSchema( + { + snippetId: num("The snippet id to update"), + name: str("New name"), + content: str("New command text"), + description: str("New description"), + folder: str("New folder"), + }, + ["snippetId"], + ), + handler: async (args) => { + const changes: Record = {}; + for (const field of ["name", "content", "description", "folder"]) { + if (args[field] !== undefined) changes[field] = args[field]; + } + return proposal( + "propose_update_snippet", + `Update snippet ${String(args.snippetId)}`, + { snippetId: args.snippetId, changes }, + ); + }, + }, + { + name: "propose_delete_snippet", + description: "Propose deleting a snippet.", + category: "propose", + parameters: objectSchema( + { + snippetId: num("The snippet id to delete"), + reason: str("Why this snippet should be removed"), + }, + ["snippetId", "reason"], + ), + handler: async (args) => + proposal( + "propose_delete_snippet", + `Delete snippet ${String(args.snippetId)}`, + { snippetId: args.snippetId, reason: args.reason }, + ), + }, + { + name: "propose_create_automation", + description: + "Propose a new automation. The definition must be a valid AutomationDefinition object with a trigger and an ordered list of steps. It is validated server-side and previewed with a dry run before anything happens.", + category: "propose", + parameters: objectSchema( + { + name: str("Automation name"), + description: str("What the automation does"), + definition: { + type: "object", + description: + "The AutomationDefinition: { trigger: {...}, steps: [...] }", + }, + }, + ["name", "definition"], + ), + handler: async (args) => + proposal( + "propose_create_automation", + `Create automation ${String(args.name)}`, + { + name: args.name, + description: args.description ?? null, + definition: args.definition, + }, + ), + }, + { + name: "propose_create_fleet", + description: "Propose grouping hosts into a new fleet.", + category: "propose", + parameters: objectSchema( + { + name: str("Fleet name"), + description: str("What this fleet is for"), + hostIds: { + type: "array", + items: { type: "number" }, + description: "Host ids to add as members", + }, + }, + ["name"], + ), + handler: async (args) => + proposal("propose_create_fleet", `Create fleet ${String(args.name)}`, { + name: args.name, + description: args.description ?? null, + hostIds: args.hostIds ?? [], + }), + }, + { + name: "propose_create_alert_rule", + description: + "Propose a new alert rule that fires when a host metric crosses a threshold.", + category: "propose", + parameters: objectSchema( + { + name: str("Rule name"), + hostId: num("Host id to watch, or omit to watch all hosts"), + triggerType: str( + "What to watch, for example cpu, memory, disk or host_status", + ), + thresholdValue: num("Threshold to compare against"), + thresholdDurationSeconds: num( + "How long the breach must persist before firing", + ), + cooldownMinutes: num("Minimum minutes between repeat firings"), + }, + ["name", "triggerType"], + ), + handler: async (args) => + proposal( + "propose_create_alert_rule", + `Create alert rule ${String(args.name)}`, + { + name: args.name, + hostId: args.hostId ?? null, + triggerType: args.triggerType, + thresholdValue: args.thresholdValue ?? null, + thresholdDurationSeconds: args.thresholdDurationSeconds ?? null, + cooldownMinutes: args.cooldownMinutes ?? 15, + }, + ), + }, + { + name: "propose_run_command", + description: + "Propose running a command on a host. The user reviews and approves it before it runs. Use this for anything that changes state; read-only diagnostics may run directly if the user has enabled that.", + category: "propose", + parameters: objectSchema( + { + hostId: num("The host id to run on"), + command: str("The exact command to run"), + explanation: str("What the command does and why it is needed"), + }, + ["hostId", "command", "explanation"], + ), + handler: async (args) => + proposal( + "propose_run_command", + // Deliberately short: the card renders the command in its own block, + // so repeating it here made every card twice as tall as it needed. + `Run a command on host ${String(args.hostId)}`, + { + hostId: args.hostId, + command: args.command, + explanation: args.explanation, + }, + ), + }, +]; diff --git a/src/backend/ai/tools/read-tools.ts b/src/backend/ai/tools/read-tools.ts new file mode 100644 index 0000000..77ec7e3 --- /dev/null +++ b/src/backend/ai/tools/read-tools.ts @@ -0,0 +1,331 @@ +import { + createCurrentAlertRepository, + createCurrentAutomationRepository, + createCurrentCommandHistoryRepository, + createCurrentFleetRepository, + createCurrentHomepageItemRepository, + createCurrentHostRepository, + createCurrentNetworkTopologyRepository, + createCurrentSnippetRepository, + createCurrentWorkspaceRepository, +} from "../../database/repositories/factory.js"; +import { num, objectSchema, type AiTool } from "./types.js"; + +/** + * Read tools project explicit fields rather than spreading rows. Redaction runs + * afterwards as a second line of defense, but the projection here is the + * primary control: a field that is never selected cannot leak. + */ + +interface HostSummary { + id: number; + name: string | null; + ip: string | null; + port: number | null; + username: string | null; + folder: string | null; + tags: unknown; + protocol: string | null; + enableTerminal: boolean | null; + enableFileManager: boolean | null; + enableTunnel: boolean | null; + enableDocker: boolean | null; +} + +function toHostSummary(host: Record): HostSummary { + return { + id: host.id, + name: host.name ?? null, + ip: host.ip ?? null, + port: host.port ?? null, + username: host.username ?? null, + folder: host.folder ?? null, + tags: host.tags ?? null, + protocol: host.protocol ?? null, + enableTerminal: host.enableTerminal ?? null, + enableFileManager: host.enableFileManager ?? null, + enableTunnel: host.enableTunnel ?? null, + enableDocker: host.enableDocker ?? null, + }; +} + +export const readTools: AiTool[] = [ + { + name: "list_hosts", + description: + "List the user's SSH hosts with their names, addresses, folders and tags. Never returns passwords or keys. Call this before proposing anything that references a host.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const hosts = await createCurrentHostRepository().listByUserId( + context.userId, + ); + return { hosts: hosts.map((host) => toHostSummary(host as any)) }; + }, + }, + { + name: "get_host", + description: + "Get one host's non-secret configuration by id. Use it to inspect settings before proposing an update.", + category: "read", + parameters: objectSchema( + { hostId: num("The host id, as returned by list_hosts") }, + ["hostId"], + ), + handler: async (args, context) => { + const hostId = Number(args.hostId); + const host = await createCurrentHostRepository().findByIdForUser( + context.userId, + hostId, + ); + if (!host) return { error: "Host not found" }; + return { host: toHostSummary(host as any) }; + }, + }, + { + name: "list_fleets", + description: + "List the user's fleets. Fleets group hosts for bulk operations and inventory.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const fleets = await createCurrentFleetRepository().listByUser( + context.userId, + ); + return { + fleets: fleets.map((fleet: any) => ({ + id: fleet.id, + name: fleet.name, + description: fleet.description ?? null, + })), + }; + }, + }, + { + name: "list_snippets", + description: + "List the user's saved command snippets, including their folder and the command text.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const snippets = await createCurrentSnippetRepository().listOwnedSnippets( + context.userId, + ); + return { + snippets: snippets.map((snippet: any) => ({ + id: snippet.id, + name: snippet.name, + content: snippet.content, + folder: snippet.folder ?? null, + description: snippet.description ?? null, + })), + }; + }, + }, + { + name: "list_automations", + description: + "List the user's automations with their trigger kind and enabled state. Read this before proposing a change to an existing automation.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const automations = await createCurrentAutomationRepository().list( + context.userId, + ); + return { + automations: automations.map((automation: any) => ({ + id: automation.id, + name: automation.name, + description: automation.description ?? null, + enabled: automation.enabled, + lastRunAt: automation.lastRunAt ?? null, + lastRunStatus: automation.lastRunStatus ?? null, + })), + }; + }, + }, + { + name: "get_automation", + description: + "Get one automation's full definition (trigger and steps) by id.", + category: "read", + parameters: objectSchema( + { + automationId: num("The automation id, as returned by list_automations"), + }, + ["automationId"], + ), + handler: async (args, context) => { + const automation = await createCurrentAutomationRepository().findForUser( + Number(args.automationId), + context.userId, + ); + if (!automation) return { error: "Automation not found" }; + return { + automation: { + id: (automation as any).id, + name: (automation as any).name, + enabled: (automation as any).enabled, + definition: (automation as any).definition, + }, + }; + }, + }, + { + name: "list_workspaces", + description: + "List the user's saved workspace layouts (named sets of open tabs and splits).", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const workspaces = await createCurrentWorkspaceRepository().listByUser( + context.userId, + ); + return { + workspaces: workspaces.map((workspace: any) => ({ + id: workspace.id, + name: workspace.name, + isDefault: workspace.isDefault ?? false, + })), + }; + }, + }, + { + name: "list_alert_rules", + description: + "List the user's alert rules with their thresholds and enabled state.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const rules = await createCurrentAlertRepository().listAlertRules( + context.userId, + ); + return { + rules: rules.map((rule) => ({ + id: rule.id, + name: rule.name, + hostId: rule.host_id, + enabled: rule.enabled === 1, + triggerType: rule.trigger_type, + thresholdValue: rule.threshold_value, + thresholdDurationSeconds: rule.threshold_duration_seconds, + cooldownMinutes: rule.cooldown_minutes, + })), + }; + }, + }, + { + name: "list_notification_channels", + description: + "List the user's notification channels by id, name and type. Channel configuration is never returned because it holds tokens.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const channels = + await createCurrentAlertRepository().listNotificationChannels( + context.userId, + ); + return { + channels: channels.map((channel) => ({ + id: channel.id, + name: channel.name, + type: channel.type, + enabled: channel.enabled === 1, + })), + }; + }, + }, + { + name: "get_alert_firings", + description: + "Recent alert firings, newest first. Use this to answer questions about what has been alerting.", + category: "read", + parameters: objectSchema({ + limit: num("How many firings to return (default 25, max 100)"), + }), + handler: async (args, context) => { + const limit = Math.min(Math.max(Number(args.limit) || 25, 1), 100); + const result = await createCurrentAlertRepository().listAlertFirings({ + userId: context.userId, + limit, + offset: 0, + }); + return { + firings: (result.firings ?? []).map((firing) => ({ + id: firing.id, + ruleName: firing.rule_name, + hostName: firing.host_name, + firedAt: firing.fired_at, + resolvedAt: firing.resolved_at, + severity: firing.severity, + message: firing.message, + acknowledged: firing.acknowledged === 1, + })), + }; + }, + }, + { + name: "list_homepage_items", + description: "List the user's homepage service-link tiles.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const items = await createCurrentHomepageItemRepository().listByUserId( + context.userId, + ); + return { + items: (items as any[]).map((item) => ({ + id: item.id, + typeId: item.typeId, + title: item.title ?? null, + })), + }; + }, + }, + { + name: "get_command_history", + description: + "Recent commands the user has run on one host, newest first. Useful for understanding what they have been working on.", + category: "read", + parameters: objectSchema( + { + hostId: num("The host id, as returned by list_hosts"), + limit: num("How many entries to return (default 25, max 100)"), + }, + ["hostId"], + ), + handler: async (args, context) => { + const limit = Math.min(Math.max(Number(args.limit) || 25, 1), 100); + const hostId = Number(args.hostId); + + // Ownership is enforced here rather than trusted from the model. + const host = await createCurrentHostRepository().findByIdForUser( + context.userId, + hostId, + ); + if (!host) return { error: "Host not found" }; + + const commands = + await createCurrentCommandHistoryRepository().listCommandsForHost( + context.userId, + hostId, + limit, + ); + return { commands }; + }, + }, + { + name: "get_network_topology", + description: "The user's saved network topology graph, if they have one.", + category: "read", + parameters: objectSchema({}), + handler: async (_args, context) => { + const topology = + await createCurrentNetworkTopologyRepository().findByUserId( + context.userId, + ); + if (!topology) return { topology: null }; + return { topology: (topology as any).data ?? null }; + }, + }, +]; diff --git a/src/backend/ai/tools/types.ts b/src/backend/ai/tools/types.ts new file mode 100644 index 0000000..34117dc --- /dev/null +++ b/src/backend/ai/tools/types.ts @@ -0,0 +1,72 @@ +export type ToolCategory = "read" | "propose"; + +export interface ToolContext { + /** Always taken from the verified JWT, never from model input. */ + userId: string; + conversationId: number; + /** Per-user opt-in for running allowlisted read-only commands. */ + allowReadOnlyCommands: boolean; +} + +export interface AiTool { + name: string; + description: string; + category: ToolCategory; + /** JSON Schema for the arguments, sent to the provider verbatim. */ + parameters: Record; + /** + * Read tools return data to feed back to the model. Propose tools return a + * ProposalDraft and must not mutate anything. + */ + handler: ( + args: Record, + context: ToolContext, + ) => Promise; +} + +/** What a provider adapter needs to describe a tool to its model. */ +export interface ToolDefinitionShape { + name: string; + description: string; + parameters: Record; +} + +export interface ProposalDraft { + __proposal: true; + kind: string; + summary: string; + payload: Record; +} + +export function isProposalDraft(value: unknown): value is ProposalDraft { + return ( + typeof value === "object" && + value !== null && + (value as ProposalDraft).__proposal === true + ); +} + +export function proposal( + kind: string, + summary: string, + payload: Record, +): ProposalDraft { + return { __proposal: true, kind, summary, payload }; +} + +/** Small helper so tool schemas stay readable. */ +export function objectSchema( + properties: Record, + required: string[] = [], +): Record { + return { + type: "object", + properties, + required, + additionalProperties: false, + }; +} + +export const str = (description: string) => ({ type: "string", description }); +export const num = (description: string) => ({ type: "number", description }); +export const bool = (description: string) => ({ type: "boolean", description }); diff --git a/src/backend/automations/actions/host-targets.ts b/src/backend/automations/actions/host-targets.ts new file mode 100644 index 0000000..adb9366 --- /dev/null +++ b/src/backend/automations/actions/host-targets.ts @@ -0,0 +1,90 @@ +import type { HostSelector } from "../../../types/automations.js"; +import { resolveHostById } from "../../hosts/host-resolver.js"; +import { createCurrentFleetRepository } from "../../database/repositories/factory.js"; +import { PermissionManager } from "../../utils/permission-manager.js"; +import type { StepExecutionContext } from "./types.js"; + +/** A host the caller is allowed to act on. `host` is always resolved. */ +export interface ResolvedTarget { + id: number; + name: string; + host: NonNullable>>; +} + +/** + * Turns a selector into the hosts a step may actually act on. + * + * Access is checked here, at execution time rather than when the automation + * was saved, so a permission revoked after the fact takes effect on the next + * run. resolveHostById performs its own connect-level check and returns null + * when the owner can no longer reach the host. + */ +export async function resolveTargets( + selector: HostSelector, + context: StepExecutionContext, +): Promise<{ targets: ResolvedTarget[]; skipped: number[] }> { + const ids = await selectorHostIds(selector, context); + const targets: ResolvedTarget[] = []; + const skipped: number[] = []; + + for (const id of ids) { + const host = await resolveHostById(id, context.userId); + if (!host) { + skipped.push(id); + continue; + } + targets.push({ id, name: host.name || host.ip, host }); + } + + return { targets, skipped }; +} + +async function selectorHostIds( + selector: HostSelector, + context: StepExecutionContext, +): Promise { + switch (selector.kind) { + case "host": + return [selector.hostId]; + case "hosts": + return selector.hostIds; + case "trigger": + return context.triggerHostId ? [context.triggerHostId] : []; + case "fleet": + return fleetHostIds(selector.fleetId, context.userId); + case "all": + return allAccessibleHostIds(context.userId); + default: + return []; + } +} + +async function fleetHostIds( + fleetId: number, + userId: string, +): Promise { + try { + const repository = createCurrentFleetRepository(); + const members = await repository.listEffectiveMembers(userId, fleetId); + return members.map((member) => member.id); + } catch { + return []; + } +} + +async function allAccessibleHostIds(userId: string): Promise { + try { + const { createCurrentHostRepository } = + await import("../../database/repositories/factory.js"); + const hosts = await createCurrentHostRepository().listByUserId(userId); + const ids = hosts.map((host) => host.id); + const allowed = + await PermissionManager.getInstance().filterAccessibleHostIds( + userId, + ids, + ); + return ids.filter((id) => allowed.has(id)); + } catch { + return []; + } +} diff --git a/src/backend/automations/actions/index.ts b/src/backend/automations/actions/index.ts new file mode 100644 index 0000000..f288df4 --- /dev/null +++ b/src/backend/automations/actions/index.ts @@ -0,0 +1,406 @@ +import { + DEFAULT_STEP_TIMEOUT_MS, + type Step, +} from "../../../types/automations.js"; +import { execCommand } from "../../hosts/metrics/widgets/common-utils.js"; +import { + execElevated, + shellSingleQuote, +} from "../../hosts/metrics/managers/exec-elevated.js"; +import { + createFleetSshFactory, + getFleetPoolKey, +} from "../../hosts/ssh-client-factory.js"; +import { withConnection } from "../../hosts/ssh-connection-pool.js"; +import { resolveSnippetCommand } from "../../database/routes/snippets-execution.js"; +import { + createCurrentAlertRepository, + createCurrentSnippetRepository, +} from "../../database/repositories/factory.js"; +import { sendAutomationNotification } from "../notify.js"; +import { automationFetch } from "../http.js"; +import { renderRecord, renderTemplate } from "../template.js"; +import { resolveTargets, type ResolvedTarget } from "./host-targets.js"; +import { + fail, + ok, + stepTimeout, + type StepExecutionContext, + type StepResult, +} from "./types.js"; + +/** + * One executor per step type. + * + * Anything that leaves Termix checks context.dryRun first and reports what it + * would have done instead of doing it, so an automation can be exercised + * safely while it is being built. + */ +export async function executeStep( + step: Step, + context: StepExecutionContext, +): Promise { + switch (step.type) { + case "notify": + return runNotify(step, context); + case "http": + return runHttp(step, context); + case "run_command": + return runCommand(step, context); + case "run_snippet": + return runSnippet(step, context); + case "docker": + return runDocker(step, context); + case "tunnel": + return runTunnel(step, context); + case "wol": + return runWol(step, context); + case "wait": + return runWait(step, context); + case "set_var": + return runSetVar(step, context); + case "stop": + return { + success: true, + halt: { status: step.status ?? "success" }, + output: `Stopped with status ${step.status ?? "success"}`, + }; + default: + return fail(`Unsupported step type: ${(step as Step).type}`); + } +} + +async function runNotify( + step: Extract, + context: StepExecutionContext, +): Promise { + const title = renderTemplate(step.title ?? "", context.template); + const body = renderTemplate(step.body ?? "", context.template); + + if (step.channelIds.length === 0) { + return fail("No notification channels selected"); + } + if (context.dryRun) { + return ok( + `Would notify ${step.channelIds.length} channel(s): ${title || body}`, + ); + } + + const repository = createCurrentAlertRepository(); + const channels = await repository.listNotificationChannels(context.userId); + const selected = channels.filter((channel) => + step.channelIds.includes(channel.id), + ); + + if (selected.length === 0) { + return fail("Selected notification channels no longer exist"); + } + + let delivered = 0; + const errors: string[] = []; + for (const channel of selected) { + if (!channel.enabled) continue; + try { + await sendAutomationNotification( + { id: channel.id, type: channel.type, config: channel.config }, + { + title, + body, + severity: step.severity ?? "warning", + context: context.template, + }, + ); + delivered++; + } catch (error) { + errors.push( + `${channel.name}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + if (delivered === 0) { + return fail(errors.join("; ") || "No enabled channels to notify"); + } + return ok( + `Notified ${delivered} channel(s)${errors.length ? `; ${errors.join("; ")}` : ""}`, + ); +} + +async function runHttp( + step: Extract, + context: StepExecutionContext, +): Promise { + const url = renderTemplate(step.url, context.template); + const headers = renderRecord(step.headers, context.template); + const body = step.body + ? renderTemplate(step.body, context.template) + : undefined; + + if (context.dryRun) { + return ok(`Would ${step.method} ${url}`); + } + + try { + const response = await automationFetch(url, { + method: step.method, + headers, + body, + allowPrivateNetwork: step.allowPrivateNetwork, + timeoutMs: stepTimeout(context, step.timeoutMs, DEFAULT_STEP_TIMEOUT_MS), + }); + + const text = await response.text(); + const summary = `HTTP ${response.status} ${response.statusText}\n${text}`; + return response.ok ? ok(summary) : fail(`HTTP ${response.status}`, summary); + } catch (error) { + return fail(error instanceof Error ? error.message : String(error)); + } +} + +async function runCommand( + step: Extract, + context: StepExecutionContext, +): Promise { + const command = renderTemplate(step.command, context.template); + return runOnTargets(step.hostSelector, context, async (target) => { + if (context.dryRun) { + return { output: `Would run on ${target.name}: ${command}` }; + } + return execOnHost( + target.host, + command, + step.elevated, + context, + step.timeoutMs, + ); + }); +} + +async function runSnippet( + step: Extract, + context: StepExecutionContext, +): Promise { + const snippet = await createCurrentSnippetRepository() + .findOwnedById(context.userId, step.snippetId) + .catch(() => null); + + if (!snippet) return fail("Snippet not found"); + if (snippet.isNote) return fail("Notes cannot be executed on a host"); + + // Template values only ever reach the snippet through inputValues, never by + // rewriting the snippet body, so automation variables cannot inject snippet + // syntax of their own. + const inputValues = renderRecord(step.inputValues, context.template) ?? {}; + + return runOnTargets(step.hostSelector, context, async (target) => { + const command = resolveSnippetCommand( + snippet.content, + { + ip: target.host.ip, + username: target.host.username, + port: target.host.port, + name: target.host.name, + }, + inputValues, + ); + + if (context.dryRun) { + return { output: `Would run snippet on ${target.name}: ${command}` }; + } + return execOnHost( + target.host, + command, + step.elevated, + context, + step.timeoutMs, + ); + }); +} + +async function runDocker( + step: Extract, + context: StepExecutionContext, +): Promise { + const container = renderTemplate(step.container, context.template); + if (!container) return fail("Container name is required"); + + return runOnTargets(step.hostSelector, context, async (target) => { + if (context.dryRun) { + return { + output: `Would ${step.action} container ${container} on ${target.name}`, + }; + } + + // The Docker HTTP routes are tied to an interactive session, so run the + // equivalent command over the same pooled SSH connection everything else + // uses. The container name is quoted because it comes from a template. + const command = `docker ${step.action} ${shellSingleQuote(container)}`; + return execOnHost(target.host, command, false, context, step.timeoutMs); + }); +} + +async function runTunnel( + step: Extract, + context: StepExecutionContext, +): Promise { + const name = renderTemplate(step.tunnelName, context.template); + if (context.dryRun) return ok(`Would ${step.action} tunnel ${name}`); + + try { + const manager = await import("../../hosts/tunnel/manager.js"); + const config = manager.tunnelConfigs?.get(name); + if (!config) return fail(`Tunnel "${name}" is not configured`); + + if (step.action === "connect") { + await manager.connectSSHTunnel(config); + return ok(`Tunnel ${name} connected`); + } + + // shouldRetry false, otherwise the manager immediately reconnects the + // tunnel the automation just asked it to drop. + manager.manualDisconnects.add(name); + await manager.handleDisconnect(name, config, false); + return ok(`Tunnel ${name} disconnected`); + } catch (error) { + return fail(error instanceof Error ? error.message : String(error)); + } +} + +async function runWol( + step: Extract, + context: StepExecutionContext, +): Promise { + const host = await resolveTargets( + { kind: "host", hostId: step.hostId }, + context, + ); + const target = host.targets[0]; + if (!target?.host) return fail("Host not found or not accessible"); + + const mac = (target.host as { macAddress?: string }).macAddress; + if (!mac) return fail("Host has no MAC address configured"); + if (context.dryRun) return ok(`Would wake ${target.name} (${mac})`); + + try { + const { sendWakeOnLan, isValidMac } = + await import("../../utils/wake-on-lan.js"); + if (!isValidMac(mac)) return fail(`Invalid MAC address: ${mac}`); + await sendWakeOnLan(mac); + return ok(`Sent magic packet to ${target.name}`); + } catch (error) { + return fail(error instanceof Error ? error.message : String(error)); + } +} + +async function runWait( + step: Extract, + context: StepExecutionContext, +): Promise { + const requested = Math.max(step.seconds, 0) * 1000; + const waitMs = Math.min( + requested, + stepTimeout(context, undefined, requested), + ); + if (context.dryRun) return ok(`Would wait ${step.seconds}s`); + + await new Promise((resolve) => setTimeout(resolve, waitMs)); + return ok(`Waited ${Math.round(waitMs / 1000)}s`); +} + +async function runSetVar( + step: Extract, + context: StepExecutionContext, +): Promise { + const value = renderTemplate(step.value, context.template); + return ok(`${step.name} = ${value}`, { [step.name]: value }); +} + +/** + * Runs a per-host action across a selector's targets. One host failing does + * not stop the others, matching how fleet execution behaves. + */ +async function runOnTargets( + selector: Extract["hostSelector"], + context: StepExecutionContext, + run: (target: ResolvedTarget) => Promise<{ output?: string; error?: string }>, +): Promise { + const { targets, skipped } = await resolveTargets(selector, context); + + if (targets.length === 0) { + return fail( + skipped.length > 0 + ? `No accessible hosts (${skipped.length} skipped)` + : "No hosts matched the selector", + ); + } + + const results = await Promise.allSettled( + targets.map(async (target) => ({ + target, + result: await run(target), + })), + ); + + const lines: string[] = []; + let failures = 0; + + for (const [index, settled] of results.entries()) { + const name = targets[index].name; + if (settled.status === "rejected") { + failures++; + lines.push(`${name}: ${String(settled.reason)}`); + continue; + } + const { result } = settled.value; + if (result.error) { + failures++; + lines.push(`${name}: ${result.error}`); + } else { + lines.push(`${name}: ${result.output ?? "ok"}`); + } + } + + if (skipped.length > 0) { + lines.push(`${skipped.length} host(s) skipped: no access`); + } + + const output = lines.join("\n"); + return failures > 0 && failures === targets.length + ? fail(`All ${failures} host(s) failed`, output) + : ok(output); +} + +async function execOnHost( + host: ResolvedTarget["host"], + command: string, + elevated: boolean | undefined, + context: StepExecutionContext, + timeoutMs: number | undefined, +): Promise<{ output?: string; error?: string }> { + const sshHost = host as ResolvedTarget["host"] & { sudoPassword?: string }; + const timeout = stepTimeout(context, timeoutMs, DEFAULT_STEP_TIMEOUT_MS); + if (timeout <= 0) return { error: "Run deadline exceeded" }; + + try { + const result = await withConnection( + getFleetPoolKey(sshHost), + createFleetSshFactory(sshHost), + async (client) => { + if (elevated) { + return execElevated(client, command, sshHost.sudoPassword, { + timeoutMs: timeout, + }); + } + return execCommand(client, command, timeout); + }, + ); + + const output = [result.stdout, result.stderr].filter(Boolean).join("\n"); + if (result.code === 0 || result.code === null) { + return { output: output || "(no output)" }; + } + return { error: `Exited with code ${result.code}`, output }; + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } +} diff --git a/src/backend/automations/actions/types.ts b/src/backend/automations/actions/types.ts new file mode 100644 index 0000000..b6f325e --- /dev/null +++ b/src/backend/automations/actions/types.ts @@ -0,0 +1,58 @@ +import type { Step } from "../../../types/automations.js"; +import type { TemplateContext } from "../template.js"; + +/** What every executor returns. `output` is what later steps can read. */ +export interface StepResult { + success: boolean; + output?: string; + error?: string; + /** Merged into the run's variables, for set_var and friends. */ + vars?: Record; + /** Set by the stop step to end the run early. */ + halt?: { status: "success" | "failed" }; +} + +export interface StepExecutionContext { + userId: string; + automationId: number; + runId: number; + /** Nothing that leaves Termix may actually happen when this is set. */ + dryRun: boolean; + template: TemplateContext; + /** Host the trigger fired for, when there was one. */ + triggerHostId?: number; + /** Automations already on the stack, to refuse recursion. */ + ancestry: number[]; + depth: number; + /** Wall-clock deadline for the whole run. */ + deadlineAt: number; + signal?: AbortSignal; +} + +export type StepExecutor = ( + step: T, + context: StepExecutionContext, +) => Promise; + +export function ok(output?: string, vars?: Record): StepResult { + return { success: true, output, vars }; +} + +export function fail(error: string, output?: string): StepResult { + return { success: false, error, output }; +} + +/** Milliseconds left before the run's overall deadline. */ +export function remainingMs(context: StepExecutionContext): number { + return Math.max(context.deadlineAt - Date.now(), 0); +} + +/** A step's timeout, clamped so it can never outlive the run. */ +export function stepTimeout( + context: StepExecutionContext, + requested: number | undefined, + fallback: number, +): number { + const wanted = requested && requested > 0 ? requested : fallback; + return Math.max(Math.min(wanted, remainingMs(context)), 0); +} diff --git a/src/backend/automations/conditions.ts b/src/backend/automations/conditions.ts new file mode 100644 index 0000000..8e1711e --- /dev/null +++ b/src/backend/automations/conditions.ts @@ -0,0 +1,203 @@ +import type { MetricPath, Operator } from "../../types/automations.js"; + +/** + * Operator evaluation and metric extraction. + * + * Pure: no database, no SSH, no clock. Everything here is driven by values the + * caller already has, which keeps the comparison rules testable on their own. + */ + +/** Shape of the metrics snapshot this module reads. Mirrors collectMetrics(). */ +export interface MetricsSnapshot { + cpu?: { + percent?: number | null; + load?: [number, number, number] | null; + } | null; + memory?: { percent?: number | null; usedGiB?: number | null } | null; + disk?: { + percent?: number | null; + filesystems?: Array<{ + mount?: string; + percent?: number | null; + availableBytes?: number | null; + }> | null; + } | null; + network?: { + interfaces?: Array<{ + name?: string; + rxBytes?: string | number | null; + txBytes?: string | number | null; + rxRateBps?: number | null; + txRateBps?: number | null; + }> | null; + } | null; + temperature?: { highestCelsius?: number | null } | null; + uptime?: { seconds?: number | null } | null; + processes?: { total?: number | null } | null; +} + +function toNumber(value: unknown): number | null { + if (value === null || value === undefined || value === "") return null; + const parsed = typeof value === "number" ? value : Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +/** + * Pulls the value a trigger watches out of a metrics snapshot. + * + * The mount and iface selectors are what let a rule watch one filesystem or + * one interface. Without a selector the aggregate is used, which for disk is + * the primary (root) mount, matching what the metrics UI shows. + */ +export function extractMetricValue( + metrics: MetricsSnapshot | null | undefined, + metric: MetricPath, +): number | null { + if (!metrics) return null; + + switch (metric.path) { + case "cpu.percent": + return toNumber(metrics.cpu?.percent); + case "cpu.load1": + return toNumber(metrics.cpu?.load?.[0]); + case "cpu.load5": + return toNumber(metrics.cpu?.load?.[1]); + case "cpu.load15": + return toNumber(metrics.cpu?.load?.[2]); + case "memory.percent": + return toNumber(metrics.memory?.percent); + case "memory.usedGiB": + return toNumber(metrics.memory?.usedGiB); + case "disk.percent": { + if (!metric.mount) return toNumber(metrics.disk?.percent); + const fs = findMount(metrics, metric.mount); + return fs ? toNumber(fs.percent) : null; + } + case "disk.availableBytes": { + const fs = metric.mount ? findMount(metrics, metric.mount) : null; + if (metric.mount) return fs ? toNumber(fs.availableBytes) : null; + const first = metrics.disk?.filesystems?.[0]; + return first ? toNumber(first.availableBytes) : null; + } + case "temperature.highestCelsius": + return toNumber(metrics.temperature?.highestCelsius); + case "uptime.seconds": + return toNumber(metrics.uptime?.seconds); + case "processes.total": + return toNumber(metrics.processes?.total); + case "network.rxBytes": + return toNumber(findInterface(metrics, metric.iface)?.rxBytes); + case "network.txBytes": + return toNumber(findInterface(metrics, metric.iface)?.txBytes); + case "network.rxRateBps": + return toNumber(findInterface(metrics, metric.iface)?.rxRateBps); + case "network.txRateBps": + return toNumber(findInterface(metrics, metric.iface)?.txRateBps); + default: + return null; + } +} + +function findMount(metrics: MetricsSnapshot, mount: string) { + return ( + metrics.disk?.filesystems?.find((entry) => entry.mount === mount) ?? null + ); +} + +function findInterface(metrics: MetricsSnapshot, iface?: string) { + const interfaces = metrics.network?.interfaces; + if (!interfaces || interfaces.length === 0) return null; + if (!iface) return interfaces[0]; + return interfaces.find((entry) => entry.name === iface) ?? null; +} + +/** + * The key a trigger's durable state is stored under. Including the mount or + * container is what allows a sustained-breach window per filesystem rather + * than per host. + */ +export function metricStateKey(hostId: number, metric: MetricPath): string { + if ("mount" in metric && metric.mount) return `${hostId}:${metric.mount}`; + if ("iface" in metric && metric.iface) return `${hostId}:${metric.iface}`; + return String(hostId); +} + +/** + * Compares two values. Numeric when both sides look numeric, so "90" and 90 + * behave the same; string comparison otherwise. `changed` is handled by the + * caller, which is the only place that knows the previous value. + */ +export function compare( + left: unknown, + operator: Operator, + right: unknown, +): boolean { + if (operator === "contains" || operator === "not_contains") { + const haystack = String(left ?? ""); + const needle = String(right ?? ""); + const found = haystack.includes(needle); + return operator === "contains" ? found : !found; + } + + const leftNumber = toNumber(left); + const rightNumber = toNumber(right); + const numeric = leftNumber !== null && rightNumber !== null; + + switch (operator) { + case ">": + return numeric && leftNumber > rightNumber; + case "<": + return numeric && leftNumber < rightNumber; + case ">=": + return numeric && leftNumber >= rightNumber; + case "<=": + return numeric && leftNumber <= rightNumber; + case "==": + return numeric + ? leftNumber === rightNumber + : String(left ?? "") === String(right ?? ""); + case "!=": + return numeric + ? leftNumber !== rightNumber + : String(left ?? "") !== String(right ?? ""); + case "changed": + return String(left ?? "") !== String(right ?? ""); + default: + return false; + } +} + +/** Whether a cooldown window is still open. */ +export function isCoolingDown( + lastFiredAt: string | null | undefined, + cooldownMinutes: number, + now: number = Date.now(), +): boolean { + if (!lastFiredAt) return false; + const last = Date.parse(lastFiredAt); + if (Number.isNaN(last)) return false; + return now - last < Math.max(cooldownMinutes, 0) * 60_000; +} + +/** Whether a sustained breach has been held long enough to fire. */ +export function hasDwelled( + breachStartedAt: string | null | undefined, + forSeconds: number | undefined, + now: number = Date.now(), +): boolean { + if (!forSeconds) return true; + if (!breachStartedAt) return false; + const started = Date.parse(breachStartedAt); + if (Number.isNaN(started)) return false; + return now - started >= forSeconds * 1000; +} + +/** Severity for a threshold breach, matching the old engine's behaviour. */ +export function severityForValue( + value: number | null, + explicit?: "info" | "warning" | "critical", +): "info" | "warning" | "critical" { + if (explicit) return explicit; + if (value !== null && value >= 95) return "critical"; + return "warning"; +} diff --git a/src/backend/automations/cron.ts b/src/backend/automations/cron.ts new file mode 100644 index 0000000..7fd2a33 --- /dev/null +++ b/src/backend/automations/cron.ts @@ -0,0 +1,291 @@ +/** + * A small five field cron parser, used only to work out when a schedule is + * next due. + * + * Deliberately not a dependency: the engine needs "when is this next due?" and + * nothing else, and a pure function is far easier to test than a scheduler + * library. Fields are the standard minute, hour, day-of-month, month and + * day-of-week, supporting *, lists (1,2), ranges (1-5) and steps (a slash). + * + * Day-of-month and day-of-week follow cron's union rule: when both are + * restricted, a date matching either one matches. + */ + +interface CronFields { + minutes: Set; + hours: Set; + daysOfMonth: Set; + months: Set; + daysOfWeek: Set; + domRestricted: boolean; + dowRestricted: boolean; +} + +const RANGES: Record = { + minute: [0, 59], + hour: [0, 23], + dayOfMonth: [1, 31], + month: [1, 12], + // 7 is accepted as an alias for Sunday and folded to 0 once parsed. + dayOfWeek: [0, 7], +}; + +const NAMED_MONTHS: Record = { + jan: 1, + feb: 2, + mar: 3, + apr: 4, + may: 5, + jun: 6, + jul: 7, + aug: 8, + sep: 9, + oct: 10, + nov: 11, + dec: 12, +}; + +const NAMED_DAYS: Record = { + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6, +}; + +function normalize(token: string, kind: string): string { + const lower = token.toLowerCase(); + if (kind === "month" && lower in NAMED_MONTHS) { + return String(NAMED_MONTHS[lower]); + } + if (kind === "dayOfWeek" && lower in NAMED_DAYS) { + return String(NAMED_DAYS[lower]); + } + return token; +} + +function parseField(field: string, kind: keyof typeof RANGES): Set { + const [min, max] = RANGES[kind]; + const values = new Set(); + + for (const part of field.split(",")) { + const trimmed = part.trim(); + if (!trimmed) throw new Error(`Empty value in ${kind} field`); + + const [rangePart, stepPart] = trimmed.split("/"); + const step = stepPart === undefined ? 1 : Number(stepPart); + if (!Number.isInteger(step) || step < 1) { + throw new Error(`Invalid step in ${kind} field`); + } + + let start: number; + let end: number; + + if (rangePart === "*" || rangePart === "") { + start = min; + end = max; + } else if (rangePart.includes("-")) { + const [from, to] = rangePart.split("-"); + start = Number(normalize(from, kind)); + end = Number(normalize(to, kind)); + } else { + start = Number(normalize(rangePart, kind)); + end = stepPart === undefined ? start : max; + } + + if (!Number.isInteger(start) || !Number.isInteger(end)) { + throw new Error(`Invalid value in ${kind} field`); + } + if (start < min || end > max || start > end) { + throw new Error(`Value out of range in ${kind} field`); + } + + for (let value = start; value <= end; value += step) { + // Sunday can be written as 7; cron treats it as 0. + values.add(kind === "dayOfWeek" && value === 7 ? 0 : value); + } + } + + return values; +} + +export function parseCron(expression: string): CronFields { + const fields = expression.trim().split(/\s+/); + if (fields.length !== 5) { + throw new Error("A cron expression needs five fields"); + } + + const [minute, hour, dayOfMonth, month, dayOfWeek] = fields; + return { + minutes: parseField(minute, "minute"), + hours: parseField(hour, "hour"), + daysOfMonth: parseField(dayOfMonth, "dayOfMonth"), + months: parseField(month, "month"), + daysOfWeek: parseField(dayOfWeek, "dayOfWeek"), + domRestricted: dayOfMonth.trim() !== "*", + dowRestricted: dayOfWeek.trim() !== "*", + }; +} + +export function isValidCron(expression: string): boolean { + try { + parseCron(expression); + return true; + } catch { + return false; + } +} + +/** Wall-clock fields for a moment, in a given zone or server local time. */ +interface WallClock { + minute: number; + hour: number; + dayOfMonth: number; + month: number; + dayOfWeek: number; +} + +const WEEKDAYS: Record = { + Sun: 0, + Mon: 1, + Tue: 2, + Wed: 3, + Thu: 4, + Fri: 5, + Sat: 6, +}; + +const formatterCache = new Map(); + +/** + * Whether a zone name is one this runtime actually knows. An unknown zone + * falls back to server local time rather than throwing, so a bad value saved + * against a schedule cannot stop it from ever running. + */ +export function isValidTimezone(timezone: string): boolean { + try { + new Intl.DateTimeFormat("en-US", { timeZone: timezone }); + return true; + } catch { + return false; + } +} + +function formatterFor(timezone: string): Intl.DateTimeFormat | null { + const cached = formatterCache.get(timezone); + if (cached) return cached; + + try { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + hour12: false, + weekday: "short", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); + formatterCache.set(timezone, formatter); + return formatter; + } catch { + return null; + } +} + +function wallClock(date: Date, timezone?: string | null): WallClock { + const formatter = timezone ? formatterFor(timezone) : null; + if (!formatter) { + return { + minute: date.getMinutes(), + hour: date.getHours(), + dayOfMonth: date.getDate(), + month: date.getMonth() + 1, + dayOfWeek: date.getDay(), + }; + } + + const parts: Record = {}; + for (const part of formatter.formatToParts(date)) { + parts[part.type] = part.value; + } + + return { + // Midnight formats as 24 in some locales' hour-cycle handling. + minute: Number(parts.minute), + hour: Number(parts.hour) % 24, + dayOfMonth: Number(parts.day), + month: Number(parts.month), + dayOfWeek: WEEKDAYS[parts.weekday] ?? date.getDay(), + }; +} + +function matches( + fields: CronFields, + date: Date, + timezone?: string | null, +): boolean { + const clock = wallClock(date, timezone); + if (!fields.months.has(clock.month)) return false; + if (!fields.minutes.has(clock.minute)) return false; + if (!fields.hours.has(clock.hour)) return false; + + const domMatch = fields.daysOfMonth.has(clock.dayOfMonth); + const dowMatch = fields.daysOfWeek.has(clock.dayOfWeek); + + // Both restricted means either may match, which is how cron behaves. + if (fields.domRestricted && fields.dowRestricted) return domMatch || dowMatch; + if (fields.domRestricted) return domMatch; + if (fields.dowRestricted) return dowMatch; + return true; +} + +/** + * The next time on or after `from` that the expression matches, or null when + * nothing matches within a four year window (e.g. Feb 30). + */ +export function nextCronRun( + expression: string, + from: Date = new Date(), + timezone?: string | null, +): Date | null { + const fields = parseCron(expression); + + const candidate = new Date(from.getTime()); + candidate.setSeconds(0, 0); + candidate.setMinutes(candidate.getMinutes() + 1); + + // Four years covers every leap year cycle, so a date that never matches + // gives up rather than looping. + const limit = 366 * 4 * 24 * 60; + for (let i = 0; i < limit; i++) { + if (matches(fields, candidate, timezone)) return candidate; + candidate.setMinutes(candidate.getMinutes() + 1); + } + return null; +} + +/** + * Next due time for a schedule trigger, as an ISO string. Interval wins over + * cron when both are set, matching the editor which offers one or the other. + */ +export function computeNextDueAt( + schedule: { + cron?: string | null; + intervalSeconds?: number | null; + timezone?: string | null; + }, + from: Date = new Date(), +): string | null { + if (schedule.intervalSeconds && schedule.intervalSeconds > 0) { + return new Date( + from.getTime() + schedule.intervalSeconds * 1000, + ).toISOString(); + } + if (schedule.cron) { + const next = nextCronRun(schedule.cron, from, schedule.timezone); + return next ? next.toISOString() : null; + } + return null; +} diff --git a/src/backend/automations/docker-watcher.ts b/src/backend/automations/docker-watcher.ts new file mode 100644 index 0000000..1626faa --- /dev/null +++ b/src/backend/automations/docker-watcher.ts @@ -0,0 +1,238 @@ +import type { AutomationDefinition } from "../../types/automations.js"; +import { createCurrentAutomationRepository } from "../database/repositories/factory.js"; +import { resolveHostById } from "../hosts/host-resolver.js"; +import { DataCrypto } from "../utils/data-crypto.js"; +import { execCommand } from "../hosts/metrics/widgets/common-utils.js"; +import { + createFleetSshFactory, + getFleetPoolKey, +} from "../hosts/ssh-client-factory.js"; +import { withConnection } from "../hosts/ssh-connection-pool.js"; +import { statsLogger } from "../utils/logger.js"; +import { onDockerEvent } from "./triggers.js"; + +/** + * Container state polling for docker_event triggers. + * + * Everything else Docker-related in the backend hangs off an interactive + * session that only exists while somebody has the UI open, so a trigger built + * on it would only ever fire while being watched. This polls over the same + * pooled SSH connection the other automation steps use, and only for hosts a + * docker_event trigger actually names, so an install with no such automation + * does no extra work at all. + * + * Events are derived by diffing successive snapshots: the poll interval is the + * resolution, so a container that stops and starts between two polls is not + * reported. That is the tradeoff for not holding a `docker events` stream open + * against every host. + */ + +const POLL_INTERVAL_MS = 60_000; +const EXEC_TIMEOUT_MS = 15_000; + +interface ContainerState { + /** Docker's own state word: running, exited, restarting, ... */ + state: string; + /** Health from the status text, when the image declares a healthcheck. */ + unhealthy: boolean; +} + +/** Last snapshot per host, so transitions can be spotted. */ +const snapshots = new Map>(); +const lastPolledAt = new Map(); + +/** Hosts named by an enabled docker_event trigger, with the owning user. */ +export async function listDockerWatchedHosts(): Promise> { + const watched = new Map(); + + try { + const rows = await createCurrentAutomationRepository().listAllEnabled(); + for (const row of rows) { + let definition: AutomationDefinition; + try { + definition = JSON.parse(row.definition) as AutomationDefinition; + } catch { + continue; + } + + const trigger = definition.trigger; + if (trigger?.kind !== "docker_event") continue; + + const selector = trigger.hostSelector; + if (selector?.kind === "host") { + watched.set(selector.hostId, row.userId); + } else if (selector?.kind === "hosts") { + for (const hostId of selector.hostIds) watched.set(hostId, row.userId); + } + // Fleet and "all" selectors are deliberately not expanded: polling every + // host a user owns for container state is far too costly to do blindly. + } + } catch { + return watched; + } + + return watched; +} + +/** + * Parses `docker ps -a` output. One JSON object per line, matching the format + * string the container routes use. + */ +export function parseContainerStates( + output: string, +): Map { + const states = new Map(); + + for (const line of output.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + + try { + const parsed = JSON.parse(trimmed) as { + name?: string; + state?: string; + status?: string; + }; + if (!parsed.name) continue; + + states.set(parsed.name, { + state: (parsed.state ?? "").toLowerCase(), + unhealthy: /\(unhealthy\)/i.test(parsed.status ?? ""), + }); + } catch { + // A partial line is not worth failing the whole poll over. + } + } + + return states; +} + +/** + * Works out which events a pair of snapshots implies. + * + * A container missing from the previous snapshot is treated as newly seen + * rather than started, so the first poll after a restart does not replay every + * running container as a fresh start event. + */ +export function diffContainerStates( + previous: Map, + current: Map, +): Array<{ container: string; event: DockerEventName }> { + const events: Array<{ container: string; event: DockerEventName }> = []; + + for (const [name, now] of current) { + const before = previous.get(name); + if (!before) continue; + + if (before.state !== now.state) { + if (now.state === "exited") + events.push({ container: name, event: "exited" }); + else if (now.state === "running") + events.push({ container: name, event: "started" }); + else if (now.state === "restarting") + events.push({ container: name, event: "restarting" }); + } + + // Health is independent of state: a container can go unhealthy while it + // stays up, which is exactly the case worth alerting on. + if (!before.unhealthy && now.unhealthy) { + events.push({ container: name, event: "unhealthy" }); + } + } + + return events; +} + +export type DockerEventName = "exited" | "started" | "unhealthy" | "restarting"; + +const PS_FORMAT = `'{"name":"{{.Names}}","state":"{{.State}}","status":"{{.Status}}"}'`; + +async function pollHost(hostId: number, userId: string): Promise { + const host = await resolveHostById(hostId, userId); + if (!host) { + snapshots.delete(hostId); + return; + } + + const result = await withConnection( + getFleetPoolKey(host as never), + createFleetSshFactory(host as never), + (client) => + execCommand( + client, + `docker ps -a --format ${PS_FORMAT}`, + EXEC_TIMEOUT_MS, + ), + ); + + if (result.code !== 0 && result.code !== null) { + // Docker missing or not permitted on this host. Drop the snapshot so a + // later success is treated as a first observation rather than a diff. + snapshots.delete(hostId); + return; + } + + const current = parseContainerStates(result.stdout); + const previous = snapshots.get(hostId); + snapshots.set(hostId, current); + + if (!previous) return; + + for (const { container, event } of diffContainerStates(previous, current)) { + await onDockerEvent({ + hostId, + ownerUserId: userId, + container, + event, + }).catch(() => undefined); + } +} + +/** + * Polls every watched host whose interval has elapsed. Called from the + * automation scheduler tick rather than owning a timer of its own. + */ +export async function pollDockerEvents( + now: number = Date.now(), +): Promise { + const watched = await listDockerWatchedHosts(); + + for (const hostId of [...snapshots.keys()]) { + if (!watched.has(hostId)) { + snapshots.delete(hostId); + lastPolledAt.delete(hostId); + } + } + + for (const [hostId, userId] of watched) { + const last = lastPolledAt.get(hostId) ?? 0; + if (now - last < POLL_INTERVAL_MS) continue; + // Host credentials cannot be decrypted while the owner's key is locked. + if (!canAccess(userId)) continue; + lastPolledAt.set(hostId, now); + + try { + await pollHost(hostId, userId); + } catch (error) { + statsLogger.warn("Docker event poll failed", { + operation: "automation_docker_poll_error", + hostId, + error: error instanceof Error ? error.message : String(error), + }); + } + } +} + +function canAccess(userId: string): boolean { + try { + return DataCrypto.canUserAccessData(userId); + } catch { + return false; + } +} + +/** Clears cached state, for shutdown and tests. */ +export function resetDockerWatcher(): void { + snapshots.clear(); + lastPolledAt.clear(); +} diff --git a/src/backend/automations/engine.ts b/src/backend/automations/engine.ts new file mode 100644 index 0000000..813a2cf --- /dev/null +++ b/src/backend/automations/engine.ts @@ -0,0 +1,422 @@ +import type { + AutomationDefinition, + RunStatus, + Step, +} from "../../types/automations.js"; +import { + DEFAULT_MAX_RUN_SECONDS, + MAX_AUTOMATION_DEPTH, + MAX_STEP_OUTPUT_BYTES, +} from "../../types/automations.js"; +import { createCurrentAutomationRepository } from "../database/repositories/factory.js"; +import { statsLogger } from "../utils/logger.js"; +import { executeStep } from "./actions/index.js"; +import type { StepExecutionContext, StepResult } from "./actions/types.js"; +import { compare } from "./conditions.js"; +import { renderTemplate, type TemplateContext } from "./template.js"; + +export interface RunRequest { + automationId: number; + triggerType: string; + triggerContext?: Record; + triggerHostId?: number; + /** Overrides the automation's own dry-run flag, for "test run". */ + dryRun?: boolean; + parentRunId?: number; + ancestry?: number[]; + depth?: number; +} + +export interface RunOutcome { + runId: number | null; + status: RunStatus; + error?: string; +} + +/** + * Executes automations. + * + * Both HTTP handlers and the metrics hooks live in this same process, so this + * is a plain singleton rather than anything cross-process. State that has to + * survive a restart (cooldowns, dwell windows) lives in the database; the only + * thing held in memory is the set of runs currently in flight, which is + * meaningless after a restart anyway. + */ +export class AutomationEngine { + private static instance: AutomationEngine; + + private readonly running = new Set(); + private readonly queued = new Map(); + + static getInstance(): AutomationEngine { + if (!AutomationEngine.instance) { + AutomationEngine.instance = new AutomationEngine(); + } + return AutomationEngine.instance; + } + + isRunning(automationId: number): boolean { + return this.running.has(automationId); + } + + async run(request: RunRequest): Promise { + const repository = createCurrentAutomationRepository(); + const automation = await repository.findById(request.automationId); + + if (!automation) { + return { runId: null, status: "failed", error: "Automation not found" }; + } + + const depth = request.depth ?? 0; + const ancestry = request.ancestry ?? []; + + // Refuse recursion before anything is recorded, so a cycle cannot spin. + if (depth > MAX_AUTOMATION_DEPTH) { + return { + runId: null, + status: "failed", + error: `Maximum automation depth of ${MAX_AUTOMATION_DEPTH} exceeded`, + }; + } + if (ancestry.includes(automation.id)) { + return { + runId: null, + status: "failed", + error: `Automation ${automation.id} is already running in this chain`, + }; + } + + let definition: AutomationDefinition; + try { + definition = JSON.parse(automation.definition) as AutomationDefinition; + } catch { + return { + runId: null, + status: "failed", + error: "Automation definition is not valid JSON", + }; + } + + // A second trigger while a run is in flight is recorded as skipped rather + // than dropped silently, so the history explains what happened. + // + // The slot has to be claimed in the same tick as the check. It used to be + // claimed several awaits later, so two triggers arriving together both + // passed this test and both ran. + let claimed = false; + if (this.running.has(automation.id)) { + const policy = automation.concurrencyPolicy; + if (policy === "skip") { + const run = await repository.createRun({ + automationId: automation.id, + userId: automation.userId, + triggerType: request.triggerType, + triggerContext: JSON.stringify(request.triggerContext ?? {}), + status: "skipped", + }); + await repository.finishRun(run.id, { + status: "skipped", + error: "A previous run was still in progress", + durationMs: 0, + }); + return { runId: run.id, status: "skipped" }; + } + if (policy === "queue") { + const depthNow = this.queued.get(automation.id) ?? 0; + if (depthNow >= 5) { + return { runId: null, status: "skipped", error: "Queue is full" }; + } + this.queued.set(automation.id, depthNow + 1); + try { + await this.waitUntilFree(automation.id); + } finally { + this.queued.set( + automation.id, + (this.queued.get(automation.id) ?? 1) - 1, + ); + } + // waitUntilFree gives up on its own deadline, so the slot may still be + // taken. Only claim it when it is genuinely free. + if (!this.running.has(automation.id)) { + this.running.add(automation.id); + claimed = true; + } + } + } else { + this.running.add(automation.id); + claimed = true; + } + + const dryRun = request.dryRun ?? automation.dryRun; + const maxRunSeconds = automation.maxRunSeconds || DEFAULT_MAX_RUN_SECONDS; + const startedAt = Date.now(); + + let run: { id: number }; + try { + run = await repository.createRun({ + automationId: automation.id, + userId: automation.userId, + triggerType: request.triggerType, + triggerContext: JSON.stringify(request.triggerContext ?? {}), + status: "running", + dryRun, + parentRunId: request.parentRunId ?? null, + }); + } catch (err) { + // The slot is already claimed at this point, so it has to be given back + // here; the finally below is only reached once a run row exists. + if (claimed) this.running.delete(automation.id); + return { + runId: null, + status: "failed", + error: err instanceof Error ? err.message : String(err), + }; + } + + if (!claimed) this.running.add(automation.id); + + const template: TemplateContext = { + trigger: request.triggerContext ?? {}, + steps: {}, + vars: {}, + run: { + id: run.id, + automationId: automation.id, + startedAt: new Date(startedAt).toISOString(), + }, + }; + + const context: StepExecutionContext = { + userId: automation.userId, + automationId: automation.id, + runId: run.id, + dryRun, + template, + triggerHostId: request.triggerHostId, + ancestry: [...ancestry, automation.id], + depth, + deadlineAt: startedAt + maxRunSeconds * 1000, + }; + + let status: RunStatus = "success"; + let error: string | undefined; + + try { + const result = await this.runSteps(definition.steps ?? [], context, { + index: 0, + }); + if (result.halted?.status === "failed") { + status = "failed"; + error = "Stopped by a stop step"; + } else if (result.failed) { + status = "failed"; + error = result.error; + } + if (Date.now() >= context.deadlineAt) { + status = "timeout"; + error = `Run exceeded ${maxRunSeconds}s`; + } + } catch (err) { + status = "failed"; + error = err instanceof Error ? err.message : String(err); + } finally { + this.running.delete(automation.id); + } + + await repository.finishRun(run.id, { + status, + error: error ?? null, + durationMs: Date.now() - startedAt, + }); + + if (status === "failed") { + statsLogger.warn(`Automation "${automation.name}" failed`, { + operation: "automation_run_failed", + automationId: automation.id, + runId: run.id, + error, + }); + + // An automation_failed handler that itself fails must not re-announce + // its own failure, so the event is not emitted for runs that this event + // already started. + if (request.triggerType !== "internal_event") { + import("../hosts/metrics/automation-bridge.js") + .then(({ notifyAutomationInternalEvent }) => + notifyAutomationInternalEvent( + "automation_failed", + automation.userId, + undefined, + { + automationId: automation.id, + automationName: automation.name, + runId: run.id, + error: error ?? null, + }, + ), + ) + .catch(() => undefined); + } + } + + return { runId: run.id, status, error }; + } + + /** + * Runs a list of steps in order, descending into if/else. Returns as soon as + * a stop step halts the run or a failing step's policy says to stop. + */ + private async runSteps( + steps: Step[], + context: StepExecutionContext, + cursor: { index: number }, + ): Promise<{ + failed: boolean; + error?: string; + halted?: { status: "success" | "failed" }; + }> { + const repository = createCurrentAutomationRepository(); + + for (const step of steps) { + if (step.enabled === false) continue; + + if (Date.now() >= context.deadlineAt) { + return { failed: true, error: "Run deadline exceeded" }; + } + + const stepIndex = cursor.index++; + + if (step.type === "if") { + const left = renderTemplate(step.condition.left, context.template); + const right = renderTemplate( + step.condition.right ?? "", + context.template, + ); + const matched = compare(left, step.condition.operator, right); + + const rowId = await repository.createRunStep({ + runId: context.runId, + stepIndex, + stepId: step.id, + stepType: "if", + status: "running", + }); + await repository.finishRunStep(rowId, { + status: "success", + output: `Condition ${matched ? "matched" : "did not match"}: ${left} ${step.condition.operator} ${right}`, + }); + + const branch = matched ? step.then : (step.else ?? []); + const result = await this.runSteps(branch, context, cursor); + if (result.halted) return result; + if (result.failed) return result; + continue; + } + + if (step.type === "run_automation") { + const rowId = await repository.createRunStep({ + runId: context.runId, + stepIndex, + stepId: step.id, + stepType: step.type, + status: "running", + }); + + const nested = await this.run({ + automationId: step.automationId, + triggerType: "run_automation", + triggerContext: { parentAutomationId: context.automationId }, + triggerHostId: context.triggerHostId, + dryRun: context.dryRun, + parentRunId: context.runId, + ancestry: context.ancestry, + depth: context.depth + 1, + }); + + const nestedOk = nested.status === "success"; + await repository.finishRunStep(rowId, { + status: nestedOk ? "success" : "failed", + output: `Nested run ${nested.runId ?? "not started"}: ${nested.status}`, + error: nested.error ?? null, + }); + + if (!nestedOk && (step.onError ?? "stop") === "stop") { + return { failed: true, error: nested.error ?? "Nested run failed" }; + } + continue; + } + + const rowId = await repository.createRunStep({ + runId: context.runId, + stepIndex, + stepId: step.id, + stepType: step.type, + status: "running", + }); + + let result: StepResult; + try { + result = await executeStep(step, context); + } catch (err) { + result = { + success: false, + error: err instanceof Error ? err.message : String(err), + }; + } + + const { text, truncated } = truncate(result.output); + await repository.finishRunStep(rowId, { + status: result.success ? "success" : "failed", + output: text, + error: result.error ?? null, + truncated, + }); + + // Later steps read earlier output through {{steps..stdout}}. + context.template.steps = { + ...context.template.steps, + [step.id]: { + stdout: result.output ?? "", + code: result.success ? 0 : 1, + }, + }; + if (result.vars) { + context.template.vars = { ...context.template.vars, ...result.vars }; + } + + if (result.halt) return { failed: false, halted: result.halt }; + + if (!result.success) { + const policy = step.onError ?? "stop"; + if (policy === "stop") { + return { failed: true, error: result.error }; + } + } + } + + return { failed: false }; + } + + private async waitUntilFree(automationId: number): Promise { + const started = Date.now(); + while (this.running.has(automationId)) { + if (Date.now() - started > 60_000) return; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + } +} + +/** Keeps a single step's output from bloating the database. */ +function truncate(output: string | undefined): { + text: string | null; + truncated: boolean; +} { + if (!output) return { text: null, truncated: false }; + if (Buffer.byteLength(output, "utf8") <= MAX_STEP_OUTPUT_BYTES) { + return { text: output, truncated: false }; + } + return { + text: output.slice(0, MAX_STEP_OUTPUT_BYTES) + "\n... (truncated)", + truncated: true, + }; +} diff --git a/src/backend/automations/headless-viewer.ts b/src/backend/automations/headless-viewer.ts new file mode 100644 index 0000000..8123aff --- /dev/null +++ b/src/backend/automations/headless-viewer.ts @@ -0,0 +1,105 @@ +import { statsLogger } from "../utils/logger.js"; +import { listAutomationWatchedHosts } from "./triggers.js"; + +/** + * Keeps metric collection running for hosts an automation watches. + * + * Heavy metric collection is normally started by a UI viewer and stops when + * the last one leaves, which means threshold rules only ever evaluated while + * somebody had the host open. Automations register a synthetic viewer instead + * of bypassing that mechanism, so a real viewer arriving or leaving still + * behaves exactly as before. + * + * The catch is `cleanupInactiveViewers`, which drops any viewer whose + * heartbeat is older than 120s. Without the heartbeat below, headless polling + * would quietly stop two minutes after it started. + */ + +export interface ViewerRegistry { + registerViewer(hostId: number, sessionId: string, userId: string): void; + unregisterViewer(hostId: number, sessionId: string): void; + updateHeartbeat(sessionId: string): boolean; +} + +const SESSION_PREFIX = "automation:"; + +let registry: ViewerRegistry | null = null; +const registered = new Map(); + +export function setViewerRegistry(next: ViewerRegistry | null): void { + registry = next; +} + +export function automationSessionId(hostId: number): string { + return `${SESSION_PREFIX}${hostId}`; +} + +/** + * Brings the set of synthetic viewers in line with what the enabled + * automations currently watch, and heartbeats the ones that stay. + */ +export async function reconcileHeadlessViewers(): Promise<{ + added: number; + removed: number; + active: number; +}> { + if (!registry) return { added: 0, removed: 0, active: 0 }; + + let watched: Map; + try { + watched = await listAutomationWatchedHosts(); + } catch { + return { added: 0, removed: 0, active: registered.size }; + } + + let added = 0; + let removed = 0; + + for (const [hostId, userId] of watched) { + const sessionId = automationSessionId(hostId); + if (registered.has(hostId)) { + // Refresh before the 120s reaper would take it. + registry.updateHeartbeat(sessionId); + continue; + } + + try { + registry.registerViewer(hostId, sessionId, userId); + registered.set(hostId, userId); + added++; + } catch (error) { + statsLogger.warn("Could not start headless metrics for a host", { + operation: "automation_headless_register_error", + hostId, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + for (const hostId of [...registered.keys()]) { + if (watched.has(hostId)) continue; + try { + registry.unregisterViewer(hostId, automationSessionId(hostId)); + } catch { + // Already gone; drop it either way. + } + registered.delete(hostId); + removed++; + } + + return { added, removed, active: registered.size }; +} + +/** Drops every synthetic viewer, for shutdown and tests. */ +export function releaseHeadlessViewers(): void { + if (registry) { + for (const hostId of registered.keys()) { + try { + registry.unregisterViewer(hostId, automationSessionId(hostId)); + } catch { + // Nothing useful to do during teardown. + } + } + } + registered.clear(); +} diff --git a/src/backend/automations/http.ts b/src/backend/automations/http.ts new file mode 100644 index 0000000..db06951 --- /dev/null +++ b/src/backend/automations/http.ts @@ -0,0 +1,77 @@ +import { safeOutboundFetch } from "../utils/safe-outbound-fetch.js"; + +/** + * Outbound HTTP for automation steps and notification channels. + * + * safeOutboundFetch refuses private and loopback addresses, which is the right + * default against SSRF but also blocks the self-hosted ntfy or Gotify sitting + * on a LAN that many installs actually use. Rather than weaken the guard + * globally, a destination can opt in explicitly; everything else about the + * guard (scheme, embedded credentials, no redirects) still applies. + */ +export interface AutomationFetchOptions { + method?: string; + headers?: Record; + body?: string; + allowPrivateNetwork?: boolean; + timeoutMs?: number; +} + +export async function automationFetch( + url: string, + options: AutomationFetchOptions = {}, +): Promise { + const { + method = "GET", + headers, + body, + allowPrivateNetwork, + timeoutMs = 30_000, + } = options; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1)); + + const init: RequestInit = { + method, + headers: body + ? { "Content-Type": "application/json", ...(headers ?? {}) } + : headers, + body, + signal: controller.signal, + }; + + try { + if (allowPrivateNetwork) { + return await privateNetworkFetch(url, init); + } + return await safeOutboundFetch(url, init); + } finally { + clearTimeout(timer); + } +} + +/** + * The opt-in path. Keeps the parts of the guard that are always right and + * drops only the address blocklist. + */ +async function privateNetworkFetch( + rawUrl: string, + init: RequestInit, +): Promise { + let parsed: URL; + try { + parsed = new URL(rawUrl); + } catch { + throw new Error("Invalid URL"); + } + + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error("Only http and https URLs are allowed"); + } + if (parsed.username || parsed.password) { + throw new Error("URLs with embedded credentials are not allowed"); + } + + return fetch(rawUrl, { ...init, redirect: "error" }); +} diff --git a/src/backend/automations/notify.ts b/src/backend/automations/notify.ts new file mode 100644 index 0000000..ce44625 --- /dev/null +++ b/src/backend/automations/notify.ts @@ -0,0 +1,183 @@ +import { statsLogger } from "../utils/logger.js"; +import { automationFetch } from "./http.js"; +import type { TemplateContext } from "./template.js"; + +/** + * Notification delivery for automations. + * + * The alert engine special-cased Discord because its dispatcher only knew + * webhook and ntfy; here every transport goes through one switch, so adding a + * channel type is a single edit. + */ +export interface AutomationChannel { + id: number; + type: string; + config: string; +} + +export interface AutomationNotification { + title: string; + body: string; + severity: "info" | "warning" | "critical"; + context?: TemplateContext; +} + +const NTFY_PRIORITY: Record = { + info: 2, + warning: 3, + critical: 5, +}; + +const NTFY_TAGS: Record = { + info: "information_source", + warning: "warning", + critical: "rotating_light", +}; + +const DISCORD_COLORS: Record = { + info: 3066993, + warning: 16753920, + critical: 15158332, +}; + +export async function sendAutomationNotification( + channel: AutomationChannel, + notification: AutomationNotification, +): Promise { + let config: Record; + try { + config = JSON.parse(channel.config) as Record; + } catch { + throw new Error("Channel configuration is not valid JSON"); + } + + const allowPrivateNetwork = config.allowPrivateNetwork === true; + + switch (channel.type) { + case "webhook": + return sendWebhook(config, notification, allowPrivateNetwork); + case "ntfy": + return sendNtfy(config, notification, allowPrivateNetwork); + case "discord": + return sendDiscord(config, notification, allowPrivateNetwork); + default: + throw new Error(`Unsupported channel type: ${channel.type}`); + } +} + +function requireUrl(config: Record): string { + const url = typeof config.url === "string" ? config.url.trim() : ""; + if (!url) throw new Error("Channel is missing a URL"); + return url; +} + +async function sendWebhook( + config: Record, + notification: AutomationNotification, + allowPrivateNetwork: boolean, +): Promise { + const url = requireUrl(config); + const method = config.method === "PUT" ? "PUT" : "POST"; + const headers = + config.headers && typeof config.headers === "object" + ? (config.headers as Record) + : {}; + + const response = await automationFetch(url, { + method, + headers, + body: JSON.stringify({ + title: notification.title, + message: notification.body, + severity: notification.severity, + timestamp: new Date().toISOString(), + }), + allowPrivateNetwork, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } +} + +async function sendNtfy( + config: Record, + notification: AutomationNotification, + allowPrivateNetwork: boolean, +): Promise { + const base = requireUrl(config).replace(/\/$/, ""); + const topic = typeof config.topic === "string" ? config.topic.trim() : ""; + if (!topic) throw new Error("ntfy channel is missing a topic"); + + const headers: Record = { + Title: notification.title || "Termix automation", + Priority: String(NTFY_PRIORITY[notification.severity] ?? 3), + Tags: NTFY_TAGS[notification.severity] ?? "information_source", + }; + if (typeof config.token === "string" && config.token) { + headers.Authorization = `Bearer ${config.token}`; + } + + const response = await automationFetch(`${base}/${topic}`, { + method: "POST", + headers, + body: notification.body || notification.title, + allowPrivateNetwork, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } +} + +async function sendDiscord( + config: Record, + notification: AutomationNotification, + allowPrivateNetwork: boolean, +): Promise { + const url = requireUrl(config); + const payload: Record = { + embeds: [ + { + title: notification.title || "Termix automation", + description: notification.body || undefined, + color: DISCORD_COLORS[notification.severity] ?? 3447003, + timestamp: new Date().toISOString(), + }, + ], + }; + if (typeof config.username === "string" && config.username) { + payload.username = config.username; + } + if (typeof config.avatar_url === "string" && config.avatar_url) { + payload.avatar_url = config.avatar_url; + } + + const response = await automationFetch(url, { + method: "POST", + body: JSON.stringify(payload), + allowPrivateNetwork, + }); + + if (!response.ok) { + const detail = await response.text().catch(() => ""); + throw new Error( + `HTTP ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`, + ); + } +} + +/** Fire-and-forget wrapper for callers that must not block on delivery. */ +export function sendAutomationNotificationSafely( + channel: AutomationChannel, + notification: AutomationNotification, +): void { + sendAutomationNotification(channel, notification).catch((error) => { + statsLogger.warn("Automation notification failed", { + operation: "automation_notification_error", + channelId: channel.id, + type: channel.type, + error: error instanceof Error ? error.message : String(error), + }); + }); +} diff --git a/src/backend/automations/scheduler.ts b/src/backend/automations/scheduler.ts new file mode 100644 index 0000000..2b2cc94 --- /dev/null +++ b/src/backend/automations/scheduler.ts @@ -0,0 +1,207 @@ +import type { AutomationDefinition } from "../../types/automations.js"; +import { createCurrentAutomationRepository } from "../database/repositories/factory.js"; +import { DataCrypto } from "../utils/data-crypto.js"; +import { statsLogger } from "../utils/logger.js"; +import { computeNextDueAt } from "./cron.js"; +import { hasDwelled, isCoolingDown } from "./conditions.js"; +import { pollDockerEvents } from "./docker-watcher.js"; +import { AutomationEngine } from "./engine.js"; +import { reconcileHeadlessViewers } from "./headless-viewer.js"; + +/** + * The one timer the automations feature owns. + * + * Every other periodic job in the backend is its own module-level setInterval; + * this deliberately is not one per automation. A single tick handles due + * schedules, dwell windows that need re-checking without a fresh sample, the + * synthetic viewer heartbeat, and history pruning. + */ + +const TICK_MS = 15_000; +const STARTUP_DELAY_MS = 30_000; +const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000; +const RUN_RETENTION_DAYS = 30; +/** A "running" row older than this belongs to a process that is gone. */ +const STALE_RUN_MS = 6 * 60 * 60 * 1000; + +let tickTimer: NodeJS.Timeout | null = null; +let startupTimer: NodeJS.Timeout | null = null; +let lastPruneAt = 0; +let ticking = false; + +export function startAutomationScheduler(): void { + if (tickTimer) return; + + startupTimer = setTimeout(() => { + void tick(); + }, STARTUP_DELAY_MS); + startupTimer.unref?.(); + + tickTimer = setInterval(() => { + void tick(); + }, TICK_MS); + tickTimer.unref?.(); +} + +export function stopAutomationScheduler(): void { + if (tickTimer) clearInterval(tickTimer); + if (startupTimer) clearTimeout(startupTimer); + tickTimer = null; + startupTimer = null; +} + +/** Exposed for tests; the interval calls this. */ +export async function tick(now: Date = new Date()): Promise { + // A slow tick must not overlap the next one. + if (ticking) return; + ticking = true; + + try { + await reconcileHeadlessViewers().catch(() => undefined); + await runDueSchedules(now); + await recheckOpenBreaches(now); + await pollDockerEvents(now.getTime()).catch(() => undefined); + await pruneIfDue(now); + } catch (error) { + statsLogger.warn("Automation scheduler tick failed", { + operation: "automation_scheduler_tick_error", + error: error instanceof Error ? error.message : String(error), + }); + } finally { + ticking = false; + } +} + +async function runDueSchedules(now: Date): Promise { + const repository = createCurrentAutomationRepository(); + const due = await repository.listDueSchedules(now.toISOString()); + + for (const schedule of due) { + const automation = await repository.findById(schedule.automationId); + if (!automation) continue; + + // Background work can only touch a user's data while their key resolves. + if (!canAccess(automation.userId)) { + statsLogger.warn("Skipping scheduled automation: data key unavailable", { + operation: "automation_schedule_locked", + automationId: automation.id, + }); + continue; + } + + const nextDueAt = computeNextDueAt( + { + cron: schedule.cron, + intervalSeconds: schedule.intervalSeconds, + timezone: schedule.timezone, + }, + now, + ); + await repository.markScheduleTicked( + schedule.automationId, + nextDueAt, + now.toISOString(), + ); + + AutomationEngine.getInstance() + .run({ + automationId: schedule.automationId, + triggerType: "schedule", + triggerContext: { scheduledFor: now.toISOString() }, + }) + .catch((error) => { + statsLogger.warn("Scheduled automation failed to start", { + operation: "automation_schedule_error", + automationId: schedule.automationId, + error: error instanceof Error ? error.message : String(error), + }); + }); + } +} + +/** + * Fires sustained breaches whose window has elapsed. + * + * Without this a dwell window only completes when another sample happens to + * arrive, so a breach that starts just before polling stops would never fire. + */ +async function recheckOpenBreaches(now: Date): Promise { + const repository = createCurrentAutomationRepository(); + const open = await repository.listOpenBreaches(); + const nowMs = now.getTime(); + + for (const state of open) { + const automation = await repository.findById(state.automationId); + if (!automation || !automation.enabled) continue; + if (!canAccess(automation.userId)) continue; + + let definition: AutomationDefinition; + try { + definition = JSON.parse(automation.definition) as AutomationDefinition; + } catch { + continue; + } + + const trigger = definition.trigger; + if (trigger?.kind !== "metric_threshold") continue; + if (!trigger.forSeconds) continue; + if (!hasDwelled(state.breachStartedAt, trigger.forSeconds, nowMs)) continue; + if (isCoolingDown(state.lastFiredAt, trigger.cooldownMinutes, nowMs)) { + continue; + } + + const hostId = Number(state.stateKey.split(":")[0]); + await repository.upsertTriggerState({ + automationId: automation.id, + stateKey: state.stateKey, + lastFiredAt: now.toISOString(), + }); + + AutomationEngine.getInstance() + .run({ + automationId: automation.id, + triggerType: "metric_threshold", + triggerContext: { + hostId, + value: state.lastValue, + threshold: trigger.value, + metric: trigger.metric.path, + sustained: true, + }, + triggerHostId: Number.isFinite(hostId) ? hostId : undefined, + }) + .catch(() => undefined); + } +} + +async function pruneIfDue(now: Date): Promise { + if (now.getTime() - lastPruneAt < PRUNE_INTERVAL_MS) return; + lastPruneAt = now.getTime(); + + const repository = createCurrentAutomationRepository(); + try { + await repository.failStaleRunningRuns( + new Date(now.getTime() - STALE_RUN_MS).toISOString(), + ); + const deleted = await repository.pruneRunsOlderThan(RUN_RETENTION_DAYS); + if (deleted > 0) { + statsLogger.info(`Pruned ${deleted} old automation run(s)`, { + operation: "automation_run_prune", + deleted, + }); + } + } catch (error) { + statsLogger.warn("Automation run pruning failed", { + operation: "automation_run_prune_error", + error: error instanceof Error ? error.message : String(error), + }); + } +} + +function canAccess(userId: string): boolean { + try { + return DataCrypto.canUserAccessData(userId); + } catch { + return false; + } +} diff --git a/src/backend/automations/template.ts b/src/backend/automations/template.ts new file mode 100644 index 0000000..9efc86a --- /dev/null +++ b/src/backend/automations/template.ts @@ -0,0 +1,101 @@ +/** + * Variable substitution for automation steps. + * + * Templates read from the run context: {{host.name}}, {{trigger.value}}, + * {{steps..stdout}}, {{vars.myVar}}. Resolution always produces a + * plain string and never shell syntax; callers that build a command are + * responsible for quoting the result (see shellSingleQuote in + * hosts/metrics/managers/exec-elevated.ts). Nothing here escapes anything, + * precisely so there is one obvious place where quoting happens. + */ + +export interface TemplateContext { + host?: { + id?: number; + name?: string; + ip?: string; + username?: string; + port?: number; + }; + trigger?: Record; + steps?: Record; + vars?: Record; + run?: { id?: number; automationId?: number; startedAt?: string }; +} + +const TOKEN = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g; + +function readPath(context: TemplateContext, path: string): unknown { + const parts = path.split("."); + let current: unknown = context; + + for (const part of parts) { + if (current === null || current === undefined) return undefined; + if (typeof current !== "object") return undefined; + current = (current as Record)[part]; + } + return current; +} + +function stringify(value: unknown): string { + if (value === null || value === undefined) return ""; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + return JSON.stringify(value); +} + +/** + * Replaces every {{token}} it can resolve. An unresolvable token is left + * as-is so a typo shows up in the run output rather than silently becoming an + * empty string, which is the difference between a visible mistake and a + * command that quietly does the wrong thing. + */ +export function renderTemplate( + input: string, + context: TemplateContext, +): string { + if (!input || !input.includes("{{")) return input; + + return input.replace(TOKEN, (match, path: string) => { + const value = readPath(context, path); + return value === undefined ? match : stringify(value); + }); +} + +/** Renders every string in a flat record, leaving keys untouched. */ +export function renderRecord( + input: Record | undefined, + context: TemplateContext, +): Record | undefined { + if (!input) return undefined; + const output: Record = {}; + for (const [key, value] of Object.entries(input)) { + output[key] = renderTemplate(value, context); + } + return output; +} + +/** True when a template still has unresolved tokens after rendering. */ +export function hasUnresolvedTokens(rendered: string): boolean { + TOKEN.lastIndex = 0; + return TOKEN.test(rendered); +} + +const SECRET_KEY = /(authorization|token|password|secret|api[-_]?key|cookie)/i; + +/** + * Masks values whose key looks like a credential, for anything written to run + * history or returned by the API. + */ +export function redactSecrets( + input: Record | undefined, +): Record | undefined { + if (!input) return undefined; + const output: Record = {}; + for (const [key, value] of Object.entries(input)) { + output[key] = SECRET_KEY.test(key) ? "***" : value; + } + return output; +} diff --git a/src/backend/automations/triggers.ts b/src/backend/automations/triggers.ts new file mode 100644 index 0000000..ed88f55 --- /dev/null +++ b/src/backend/automations/triggers.ts @@ -0,0 +1,387 @@ +import type { + AutomationDefinition, + HostSelector, + Trigger, +} from "../../types/automations.js"; +import { createCurrentAutomationRepository } from "../database/repositories/factory.js"; +import type { AutomationEngineRow } from "../database/repositories/automation-repository.js"; +import { statsLogger } from "../utils/logger.js"; +import { + compare, + extractMetricValue, + hasDwelled, + isCoolingDown, + metricStateKey, + severityForValue, + type MetricsSnapshot, +} from "./conditions.js"; +import { AutomationEngine } from "./engine.js"; + +/** + * Matches events against automation triggers and decides what fires. + * + * Dwell windows and cooldowns live in automation_trigger_state rather than in + * memory, so a restart mid-breach neither loses the window nor re-fires an + * alert that already went out. + */ + +export interface MetricEvent { + hostId: number; + ownerUserId: string; + metrics: MetricsSnapshot; +} + +export interface StatusEvent { + hostId: number; + ownerUserId: string; + online: boolean; +} + +export interface HealthEvent { + hostId: number; + userId: string; + checkId: string; + ok: boolean; + detail?: string; +} + +export interface DockerEvent { + hostId: number; + ownerUserId: string; + container: string; + event: "exited" | "started" | "unhealthy" | "restarting"; +} + +export interface InternalEvent { + event: string; + userId: string; + hostId?: number; + details?: Record; +} + +interface LoadedAutomation { + row: AutomationEngineRow; + definition: AutomationDefinition; +} + +async function loadEnabledFor(userId: string): Promise { + try { + const rows = + await createCurrentAutomationRepository().listEnabledForUser(userId); + const loaded: LoadedAutomation[] = []; + for (const row of rows) { + try { + loaded.push({ + row, + definition: JSON.parse(row.definition) as AutomationDefinition, + }); + } catch { + // A malformed definition should not stop the others from evaluating. + } + } + return loaded; + } catch { + return []; + } +} + +/** Whether a selector covers a host. Ownership is checked by the caller. */ +function selectorCoversHost( + selector: HostSelector | undefined, + hostId: number, +): boolean { + if (!selector) return true; + switch (selector.kind) { + case "all": + case "trigger": + return true; + case "host": + return selector.hostId === hostId; + case "hosts": + return selector.hostIds.includes(hostId); + case "fleet": + // Fleet membership is resolved at execution time; evaluate optimistically + // so a fleet-scoped trigger still reaches the engine. + return true; + default: + return false; + } +} + +async function fire( + automation: AutomationEngineRow, + stateKey: string, + triggerType: string, + triggerContext: Record, + hostId?: number, +): Promise { + const repository = createCurrentAutomationRepository(); + await repository.upsertTriggerState({ + automationId: automation.id, + stateKey, + lastFiredAt: new Date().toISOString(), + }); + + AutomationEngine.getInstance() + .run({ + automationId: automation.id, + triggerType, + triggerContext, + triggerHostId: hostId, + }) + .catch((error) => { + statsLogger.warn("Automation run failed to start", { + operation: "automation_trigger_error", + automationId: automation.id, + error: error instanceof Error ? error.message : String(error), + }); + }); +} + +/** + * Metric samples. Called for every polled host, including hosts polled only + * because an automation asked for them. + */ +export async function onMetrics(event: MetricEvent): Promise { + const automations = await loadEnabledFor(event.ownerUserId); + const repository = createCurrentAutomationRepository(); + const now = Date.now(); + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "metric_threshold") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + + const value = extractMetricValue(event.metrics, trigger.metric); + if (value === null) continue; + + const stateKey = metricStateKey(event.hostId, trigger.metric); + const state = await repository.getTriggerState(row.id, stateKey); + const breaching = compare(value, trigger.operator, trigger.value); + + if (!breaching) { + if (state?.breachStartedAt) { + await repository.clearBreach(row.id, stateKey); + } + continue; + } + + // Open the dwell window on the first breaching sample. + if (!state?.breachStartedAt) { + await repository.upsertTriggerState({ + automationId: row.id, + stateKey, + breachStartedAt: new Date(now).toISOString(), + lastValue: value, + }); + if (trigger.forSeconds) continue; + } + + const breachStartedAt = + state?.breachStartedAt ?? new Date(now).toISOString(); + if (!hasDwelled(breachStartedAt, trigger.forSeconds, now)) continue; + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "metric_threshold", + { + hostId: event.hostId, + value, + threshold: trigger.value, + operator: trigger.operator, + metric: trigger.metric.path, + mount: "mount" in trigger.metric ? trigger.metric.mount : undefined, + severity: severityForValue(value, trigger.severity), + }, + event.hostId, + ); + } +} + +/** Host reachability transitions. Only edges fire, never steady state. */ +export async function onStatus(event: StatusEvent): Promise { + const automations = await loadEnabledFor(event.ownerUserId); + const repository = createCurrentAutomationRepository(); + const now = Date.now(); + const observed = event.online ? "online" : "offline"; + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "host_status") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + + const stateKey = String(event.hostId); + const state = await repository.getTriggerState(row.id, stateKey); + + if (state?.lastObservedState === observed) continue; + + await repository.upsertTriggerState({ + automationId: row.id, + stateKey, + lastObservedState: observed, + }); + + // The first observation establishes a baseline rather than firing, so a + // restart does not announce every host as though it just changed. + if (!state?.lastObservedState) continue; + if (trigger.to !== observed) continue; + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "host_status", + { hostId: event.hostId, status: observed }, + event.hostId, + ); + } +} + +export async function onHealthCheck(event: HealthEvent): Promise { + const automations = await loadEnabledFor(event.userId); + const repository = createCurrentAutomationRepository(); + const now = Date.now(); + const observed = event.ok ? "recovered" : "failing"; + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "health_check") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + if (trigger.checkId && trigger.checkId !== event.checkId) continue; + + const stateKey = `${event.hostId}:${event.checkId}`; + const state = await repository.getTriggerState(row.id, stateKey); + + if (state?.lastObservedState === observed) continue; + + await repository.upsertTriggerState({ + automationId: row.id, + stateKey, + lastObservedState: observed, + }); + + if (!state?.lastObservedState) continue; + if (trigger.to !== observed) continue; + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "health_check", + { + hostId: event.hostId, + checkId: event.checkId, + state: observed, + detail: event.detail, + }, + event.hostId, + ); + } +} + +export async function onDockerEvent(event: DockerEvent): Promise { + const automations = await loadEnabledFor(event.ownerUserId); + const repository = createCurrentAutomationRepository(); + const now = Date.now(); + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "docker_event") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + if (trigger.container && trigger.container !== event.container) continue; + if (trigger.event !== event.event) continue; + + const stateKey = `${event.hostId}:${event.container}`; + const state = await repository.getTriggerState(row.id, stateKey); + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "docker_event", + { + hostId: event.hostId, + container: event.container, + event: event.event, + }, + event.hostId, + ); + } +} + +export async function onInternalEvent(event: InternalEvent): Promise { + const automations = await loadEnabledFor(event.userId); + const repository = createCurrentAutomationRepository(); + const now = Date.now(); + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "internal_event") continue; + if (trigger.event !== event.event) continue; + if ( + event.hostId !== undefined && + !selectorCoversHost(trigger.hostSelector, event.hostId) + ) { + continue; + } + + const stateKey = event.hostId ? String(event.hostId) : "global"; + const state = await repository.getTriggerState(row.id, stateKey); + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "internal_event", + { event: event.event, hostId: event.hostId, ...(event.details ?? {}) }, + event.hostId, + ); + } +} + +/** + * Hosts that an enabled automation watches, so the poller knows to collect + * metrics for them even when nobody is looking. + */ +export async function listAutomationWatchedHosts(): Promise< + Map +> { + const watched = new Map(); + + try { + const rows = await createCurrentAutomationRepository().listAllEnabled(); + for (const row of rows) { + let definition: AutomationDefinition; + try { + definition = JSON.parse(row.definition) as AutomationDefinition; + } catch { + continue; + } + + const trigger: Trigger | undefined = definition.trigger; + // Only metric thresholds need heavy collection; status triggers are + // already served by the cheap reachability probe. + if (trigger?.kind !== "metric_threshold") continue; + + const selector = trigger.hostSelector; + if (selector?.kind === "host") { + watched.set(selector.hostId, row.userId); + } else if (selector?.kind === "hosts") { + for (const hostId of selector.hostIds) watched.set(hostId, row.userId); + } + // Fleet and "all" selectors are resolved by the scheduler, which can + // expand them without blocking this call. + } + } catch { + return watched; + } + + return watched; +} diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 66914a1..efec183 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -1,5 +1,7 @@ +import { getErrorMessage } from "../utils/error-message.js"; import express from "express"; import http from "http"; +import https from "https"; import bodyParser from "body-parser"; import multer from "multer"; import cookieParser from "cookie-parser"; @@ -8,21 +10,31 @@ import hostRoutes from "./routes/host.js"; import alertRoutes from "./routes/alerts.js"; import credentialsRoutes from "./routes/credentials.js"; import snippetsRoutes from "./routes/snippets.js"; +import fleetRoutes from "./routes/fleet-routes.js"; +import workspaceRoutes from "./routes/workspaces.js"; import c2sTunnelPresetRoutes from "./routes/c2s-tunnel-presets.js"; import terminalRoutes from "./routes/terminal.js"; import sessionLogRoutes from "./routes/session-log-routes.js"; import guacamoleRoutes from "../hosts/guacamole/routes.js"; +import sessionSharingRoutes from "../hosts/session-sharing/routes.js"; import networkTopologyRoutes from "./routes/network-topology.js"; import rbacRoutes from "./routes/rbac.js"; import openTabsRoutes from "./routes/open-tabs.js"; import userPreferencesRoutes from "./routes/user-preferences.js"; +import hostSidebarPreferencesRoutes from "./routes/host-sidebar-preferences.js"; +import credentialSidebarPreferencesRoutes from "./routes/credential-sidebar-preferences.js"; +import uiPreferencesRoutes from "./routes/ui-preferences.js"; import proxmoxRoutes from "./routes/proxmox.js"; import termixIdRoutes from "./routes/termix-id.js"; import { registerAuditLogRoutes } from "./routes/audit-log-routes.js"; import { registerTailscaleRoutes } from "./routes/tailscale-routes.js"; import vaultRoutes from "./routes/vault.js"; import alertRulesRoutes from "./routes/alert-rules-routes.js"; +import aiRoutes from "../ai/index.js"; +import automationsRoutes from "./routes/automations.js"; +import syncRoutes from "./routes/sync.js"; import { createCorsMiddleware } from "../utils/cors-config.js"; +import { createCompressionMiddleware } from "../utils/compression-config.js"; import fs from "fs"; import path from "path"; import os from "os"; @@ -66,6 +78,7 @@ app.set("trust proxy", true); const authManager = AuthManager.getInstance(); const authenticateJWT = authManager.createAuthMiddleware(); const requireAdmin = authManager.createAdminMiddleware(); +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware()); type SettingData = { @@ -480,7 +493,7 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => { }); res.status(500).json({ error: "Failed to generate RSS format", - details: error instanceof Error ? error.message : "Unknown error", + details: getErrorMessage(error), }); } }); @@ -711,7 +724,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { } const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const filename = `termix-export-${user[0].username}-${timestamp}.sqlite`; + const filename = `termix-export-${user.username}-${timestamp}.sqlite`; const tempPath = path.join(tempDir, filename); apiLogger.info("Creating export database", { @@ -880,7 +893,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { ); `); - const userRecord = user[0]; + const userRecord = user; const insertUser = exportDb.prepare(` INSERT INTO users (id, username, password_hash, is_admin, is_oidc, oidc_identifier, client_id, client_secret, issuer_url, authorization_url, token_url, identifier_path, name_path, scopes, totp_secret, totp_enabled, totp_backup_codes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -1135,7 +1148,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { }); res.status(500).json({ error: "Failed to export user data", - details: error instanceof Error ? error.message : "Unknown error", + details: getErrorMessage(error), }); } }); @@ -1601,7 +1614,7 @@ app.post( }); res.status(500).json({ error: "Failed to import SQLite data", - details: error instanceof Error ? error.message : "Unknown error", + details: getErrorMessage(error), }); } }, @@ -1662,7 +1675,7 @@ app.post("/database/export/preview", authenticateJWT, async (req, res) => { }); res.status(500).json({ error: "Failed to generate export preview", - details: error instanceof Error ? error.message : "Unknown error", + details: getErrorMessage(error), }); } }); @@ -1723,7 +1736,7 @@ app.post("/database/restore", requireAdmin, async (req, res) => { }); res.status(500).json({ error: "Database restore failed", - details: error instanceof Error ? error.message : "Unknown error", + details: getErrorMessage(error), }); } }); @@ -1733,20 +1746,31 @@ app.use("/host", hostRoutes); app.use("/alerts", alertRoutes); app.use("/credentials", credentialsRoutes); app.use("/snippets", snippetsRoutes); +app.use("/fleets", fleetRoutes); +app.use("/workspaces", workspaceRoutes); app.use("/c2s-tunnel-presets", c2sTunnelPresetRoutes); app.use("/terminal", terminalRoutes); app.use("/session_logs", sessionLogRoutes); app.use("/guacamole", guacamoleRoutes); +app.use("/session-sharing", sessionSharingRoutes); app.use("/network-topology", networkTopologyRoutes); app.use("/rbac", rbacRoutes); app.use("/open-tabs", openTabsRoutes); app.use("/user-preferences", userPreferencesRoutes); +app.use("/host-sidebar/preferences", hostSidebarPreferencesRoutes); +app.use("/credential-sidebar/preferences", credentialSidebarPreferencesRoutes); +app.use("/ui-preferences", uiPreferencesRoutes); app.use("/proxmox", proxmoxRoutes); app.use("/termix-id", termixIdRoutes); registerAuditLogRoutes(app, authenticateJWT); registerTailscaleRoutes(app, authenticateJWT); app.use("/vault", vaultRoutes); +// Before the alert routes, which are mounted at the root and would otherwise +// have first claim on the path. +app.use("/automations", automationsRoutes); +app.use("/ai", aiRoutes); app.use("/", alertRulesRoutes); +app.use("/sync", syncRoutes); const frontendDistPaths = [ path.join(__dirname, "../../../dist"), @@ -1909,7 +1933,7 @@ app.get( }); res.status(500).json({ error: "Failed to get migration status", - details: error instanceof Error ? error.message : "Unknown error", + details: getErrorMessage(error), }); } }, @@ -1987,7 +2011,7 @@ app.get( }); res.status(500).json({ error: "Failed to get migration history", - details: error instanceof Error ? error.message : "Unknown error", + details: getErrorMessage(error), }); } }, @@ -2025,7 +2049,54 @@ const sslConfig = AutoSSLSetup.getSSLConfig(); if (sslConfig.enabled) { databaseLogger.info(`SSL is enabled`, { operation: "ssl_info", - nginx_https_port: sslConfig.port, + ssl_port: sslConfig.port, backend_http_port: HTTP_PORT, }); + + try { + const httpsServer = https.createServer( + { + cert: fs.readFileSync(sslConfig.certPath), + key: fs.readFileSync(sslConfig.keyPath), + }, + app, + ); + + httpsServer.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + databaseLogger.error( + `SSL port ${sslConfig.port} is already in use. Kill the existing process and retry.`, + err, + { + operation: "https_server_port_conflict", + port: sslConfig.port, + }, + ); + return; + } + databaseLogger.error("HTTPS server error", err, { + operation: "https_server_error", + }); + }); + + httpsServer.listen(sslConfig.port, () => { + databaseLogger.success( + `Backend is now also listening for HTTPS directly`, + { + operation: "https_server_started", + port: sslConfig.port, + }, + ); + }); + } catch (error) { + databaseLogger.error( + "Failed to start HTTPS server with configured SSL certificate", + error, + { + operation: "https_server_start_failed", + cert_path: sslConfig.certPath, + key_path: sslConfig.keyPath, + }, + ); + } } diff --git a/src/backend/database/db/connect.ts b/src/backend/database/db/connect.ts new file mode 100644 index 0000000..4507040 --- /dev/null +++ b/src/backend/database/db/connect.ts @@ -0,0 +1,131 @@ +import type { DatabaseDialect } from "./dialect.js"; +import type { PortableDatabase } from "../repositories/database-context.js"; + +export const DATABASE_URL_ENV = "DATABASE_URL"; +export const DATABASE_POOL_MAX_ENV = "DATABASE_POOL_MAX"; +export const DATABASE_SSL_ENV = "DATABASE_SSL"; + +const DEFAULT_POOL_MAX = 10; + +/** + * Pool size. Both drivers default to 10; this exists so an install that runs + * several replicas against one server can keep the total connection count under + * the server's own limit, which is the usual thing to hit first. + */ +export function poolMax(env: NodeJS.ProcessEnv = process.env): number { + const raw = env[DATABASE_POOL_MAX_ENV]?.trim(); + if (!raw) return DEFAULT_POOL_MAX; + + const parsed = Number(raw); + if (!Number.isInteger(parsed) || parsed < 1) { + throw new Error( + `${DATABASE_POOL_MAX_ENV} must be a positive integer, got "${raw}".`, + ); + } + return parsed; +} + +/** + * TLS mode. + * + * "require" verifies the server certificate; "no-verify" encrypts without + * checking it, which is what a self-signed certificate on a private network + * needs. Unset means no TLS, preserving the behaviour of every install that + * predates this setting. + */ +export function sslOption( + env: NodeJS.ProcessEnv = process.env, +): false | { rejectUnauthorized: boolean } { + const raw = env[DATABASE_SSL_ENV]?.trim().toLowerCase(); + if (!raw || raw === "false" || raw === "disable") return false; + + if (raw === "true" || raw === "require") return { rejectUnauthorized: true }; + if (raw === "no-verify") return { rejectUnauthorized: false }; + + throw new Error( + `Unsupported ${DATABASE_SSL_ENV}: "${raw}". Expected require, no-verify, or disable.`, + ); +} + +/** + * Opens a connection to a client-server engine. + * + * SQLite is not handled here โ€” it has its own lifecycle in db/index.ts, where + * the database is decrypted into memory and serialised back to a file. This + * covers the engines that connect to something already running. + * + * The returned handle is typed as PortableDatabase; see the note there on why + * that is an approximation and what guarantees it. + */ +export function databaseUrl(env: NodeJS.ProcessEnv = process.env): string | null { + const url = env[DATABASE_URL_ENV]?.trim(); + return url ? url : null; +} + +/** + * Checks the connection string suits the configured engine before trying to + * open it, so a mismatch fails with something readable rather than a driver + * error thirty frames down. + */ +export function assertUrlMatchesDialect( + url: string, + dialect: DatabaseDialect, +): void { + const scheme = url.split("://", 1)[0].toLowerCase(); + + const expected: Record = { + postgres: ["postgres", "postgresql"], + mysql: ["mysql", "mariadb"], + }; + + const allowed = expected[dialect]; + if (!allowed) { + throw new Error(`${dialect} does not use ${DATABASE_URL_ENV}`); + } + + if (!allowed.includes(scheme)) { + throw new Error( + `${DATABASE_URL_ENV} is a "${scheme}://" URL but DATABASE_DIALECT is "${dialect}". ` + + `Expected one of ${allowed.map((s) => `${s}://`).join(", ")}.`, + ); + } +} + +export async function connectRemoteDatabase( + dialect: DatabaseDialect, + env: NodeJS.ProcessEnv = process.env, +): Promise { + const url = databaseUrl(env); + if (!url) { + throw new Error( + `${DATABASE_URL_ENV} must be set when DATABASE_DIALECT is "${dialect}".`, + ); + } + + assertUrlMatchesDialect(url, dialect); + + const max = poolMax(env); + const ssl = sslOption(env); + + // No `schema` option: it only feeds drizzle's relational query API + // (`db.query.*`), which nothing here uses. The query builder takes its table + // names and value encoders from the table objects the repositories import โ€” + // see the note in schema.pg.ts on why the generated schemas are DDL-only. + if (dialect === "postgres") { + const { drizzle } = await import("drizzle-orm/node-postgres"); + return drizzle({ + connection: { connectionString: url, max, ...(ssl ? { ssl } : {}) }, + }) as unknown as PortableDatabase; + } + + // mysql2 names the pool limit differently and wants no `ssl` key at all when + // TLS is off โ€” passing false is not the same as omitting it. + const { drizzle } = await import("drizzle-orm/mysql2"); + return drizzle({ + connection: { + uri: url, + connectionLimit: max, + ...(ssl ? { ssl } : {}), + }, + }) as unknown as PortableDatabase; +} diff --git a/src/backend/database/db/dialect.ts b/src/backend/database/db/dialect.ts new file mode 100644 index 0000000..171017b --- /dev/null +++ b/src/backend/database/db/dialect.ts @@ -0,0 +1,50 @@ +/** + * Which engine the schema and repositories are built against. + * + * SQLite is not going away: the desktop app embeds its backend and cannot ship + * a database server, so it will always run on SQLite. Postgres and MySQL are + * for self-hosted deployments that need more than one process to reach the + * data. This is a multi-backend story, not a migration off SQLite. + */ +export type DatabaseDialect = "sqlite" | "postgres" | "mysql"; + +export const DATABASE_DIALECT_ENV = "DATABASE_DIALECT"; + +const SUPPORTED: readonly DatabaseDialect[] = ["sqlite", "postgres", "mysql"]; + +export function isDatabaseDialect(value: unknown): value is DatabaseDialect { + return ( + typeof value === "string" && + (SUPPORTED as readonly string[]).includes(value) + ); +} + +/** + * Resolves the configured dialect, defaulting to SQLite so existing + * deployments and the desktop build are unaffected by this being added. + */ +export function resolveDatabaseDialect( + env: NodeJS.ProcessEnv = process.env, +): DatabaseDialect { + const raw = env[DATABASE_DIALECT_ENV]?.trim().toLowerCase(); + if (!raw) return "sqlite"; + + if (!isDatabaseDialect(raw)) { + throw new Error( + `Unsupported ${DATABASE_DIALECT_ENV}: "${raw}". Expected one of ${SUPPORTED.join(", ")}.`, + ); + } + return raw; +} + +/** + * Whether a write has to be explicitly persisted after it commits. + * + * SQLite here is an in-memory database serialised back to an encrypted file, so + * every write needs a trigger to flush it. Client-server engines have already + * durably committed by the time the query returns โ€” there is no file to write + * and nothing to schedule. + */ +export function needsExplicitPersist(dialect: DatabaseDialect): boolean { + return dialect === "sqlite"; +} diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index f9f519c..f015182 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import { drizzle } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; import * as schema from "./schema.js"; @@ -7,8 +8,22 @@ import { databaseLogger } from "../../utils/logger.js"; import { DatabaseFileEncryption } from "../../utils/database-file-encryption.js"; import { SystemCrypto } from "../../utils/system-crypto.js"; import { DatabaseMigration } from "../../utils/database-migration.js"; +import { + ensureSharedHostAuthOverrideProtocolSchema, + migrateLegacySharedHostAuthOverrides, +} from "../../utils/shared-host-auth-override-migration.js"; import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; +import { migrateAuditRetention } from "../../utils/audit-retention-migration.js"; +import { createPerformanceIndexes } from "./performance-indexes.js"; +import { + assertDataDirIsNotMisconfigured, + DataDirMisconfiguredError, +} from "../../utils/data-dir-guard.js"; import { getDefaultGuacdUrl } from "../../utils/guacd-config.js"; +import { resolveDatabaseDialect, type DatabaseDialect } from "./dialect.js"; +import { connectRemoteDatabase } from "./connect.js"; +import { runRemoteMigrations } from "./migrate.js"; +import type { PortableDatabase } from "../repositories/database-context.js"; const dataDir = process.env.DATA_DIR || "./db/data"; const dbDir = path.resolve(dataDir); @@ -168,18 +183,25 @@ async function initializeDatabaseAsync(): Promise { ); } } else { + assertDataDirIsNotMisconfigured(dataDir); memoryDatabase = new Database(":memory:"); isNewDatabase = true; } } } catch (error) { + // Already a precise diagnosis of an intact-but-unusable file. The + // generic decryption error below would only obscure it. if (error instanceof UnreadableDatabaseFileError) { throw error; } + // Not a decryption problem: the database is fine, we are pointed at the + // wrong directory. Surface that message as-is. + if (error instanceof DataDirMisconfiguredError) throw error; + databaseLogger.error("Failed to initialize memory database", error, { operation: "db_memory_init_failed", - errorMessage: error instanceof Error ? error.message : "Unknown error", + errorMessage: getErrorMessage(error), errorStack: error instanceof Error ? error.stack : undefined, encryptedDbExists: DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath), @@ -203,18 +225,45 @@ async function initializeDatabaseAsync(): Promise { databaseLogger.warn("Failed to generate diagnostic information", { operation: "db_diagnostic_failed", error: - diagError instanceof Error ? diagError.message : "Unknown error", + getErrorMessage(diagError), }); } throw new Error( - `Database decryption failed: ${error instanceof Error ? error.message : "Unknown error"}. This prevents data loss.`, + `Database decryption failed: ${getErrorMessage(error)}. This prevents data loss.`, { cause: error }, ); } } else { - memoryDatabase = new Database(":memory:"); - isNewDatabase = true; + assertDataDirIsNotMisconfigured(dataDir); + + // The database still lives in memory and is serialised out on every write; + // turning encryption off only changes whether that file is ciphertext. It + // has to be read back, or each restart starts empty and silently discards + // everything the previous run saved. + const existing = readPlainDatabaseFile(); + if (existing) { + memoryDatabase = new Database(existing); + databaseLogger.info("Loaded unencrypted database from disk", { + operation: "db_load_plain", + path: dbPath, + bytes: existing.length, + }); + } else { + memoryDatabase = new Database(":memory:"); + isNewDatabase = true; + } + } +} + +/** The plain database file, or null when there is nothing to restore. */ +function readPlainDatabaseFile(): Buffer | null { + try { + const contents = fs.readFileSync(dbPath); + return contents.length > 0 ? contents : null; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw error; } } @@ -315,6 +364,7 @@ async function initializeCompleteDatabase(): Promise { folder TEXT, tags TEXT, pin INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER, auth_type TEXT NOT NULL, password TEXT, key TEXT, @@ -458,9 +508,12 @@ async function initializeCompleteDatabase(): Promise { name TEXT NOT NULL, color TEXT, icon TEXT, + credential_id INTEGER, + sort_order INTEGER, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (credential_id) REFERENCES ssh_credentials (id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS recent_activity ( @@ -526,7 +579,7 @@ async function initializeCompleteDatabase(): Promise { CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, + user_id TEXT, username TEXT NOT NULL, action TEXT NOT NULL, resource_type TEXT NOT NULL, @@ -538,13 +591,14 @@ async function initializeCompleteDatabase(): Promise { success INTEGER NOT NULL, error_message TEXT, timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS session_recordings ( id INTEGER PRIMARY KEY AUTOINCREMENT, host_id INTEGER NOT NULL, - user_id TEXT NOT NULL, + user_id TEXT, + username TEXT, access_id INTEGER, started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, ended_at TEXT, @@ -557,10 +611,42 @@ async function initializeCompleteDatabase(): Promise { terminated_by_owner INTEGER DEFAULT 0, termination_reason TEXT, FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL, FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL ); + CREATE TABLE IF NOT EXISTS session_shares ( + id TEXT PRIMARY KEY, + host_id INTEGER NOT NULL, + owner_user_id TEXT NOT NULL, + protocol TEXT NOT NULL, + session_id TEXT NOT NULL, + tab_instance_id TEXT, + share_type TEXT NOT NULL, + target_user_id TEXT, + link_token TEXT UNIQUE, + permission_level TEXT NOT NULL DEFAULT 'read-only', + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TEXT NOT NULL, + revoked_at TEXT, + last_joined_at TEXT, + join_count INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS session_share_participants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + share_id TEXT NOT NULL, + user_id TEXT, + guest_label TEXT, + joined_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + left_at TEXT, + FOREIGN KEY (share_id) REFERENCES session_shares (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS api_keys ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, @@ -613,6 +699,41 @@ async function initializeCompleteDatabase(): Promise { CREATE UNIQUE INDEX IF NOT EXISTS idx_host_metrics_prefs_user_host ON host_metrics_preferences (user_id, host_id); + CREATE TABLE IF NOT EXISTS proxmox_stats_preferences ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + layout TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE + ); + + CREATE UNIQUE INDEX IF NOT EXISTS idx_proxmox_stats_prefs_user_host + ON proxmox_stats_preferences (user_id, host_id); + + CREATE TABLE IF NOT EXISTS host_sidebar_preferences ( + user_id TEXT PRIMARY KEY, + data TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS credential_sidebar_preferences ( + user_id TEXT PRIMARY KEY, + data TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS ui_preferences ( + user_id TEXT PRIMARY KEY, + data TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS host_health_checks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, @@ -705,6 +826,7 @@ async function initializeCompleteDatabase(): Promise { } migrateSchema(); + vacuumIfFreelistBloated(); try { ensureRawSettingDefault("allow_registration", "true"); @@ -760,12 +882,16 @@ const addColumnIfNotExists = ( sqlite.exec(`ALTER TABLE ${table} ADD COLUMN "${column}" ${definition};`); } catch (alterError) { - databaseLogger.warn(`Failed to add column ${column} to ${table}`, { - operation: "schema_migration", - table, - column, - error: alterError, - }); + const message = + alterError instanceof Error ? alterError.message : String(alterError); + databaseLogger.warn( + `Failed to add column ${column} to ${table}: ${message}`, + { + operation: "schema_migration", + table, + column, + }, + ); } } }; @@ -802,8 +928,15 @@ const migrateSchema = () => { addColumnIfNotExists("user_preferences", "disable_update_check", "INTEGER"); addColumnIfNotExists("user_preferences", "confirm_tab_close", "INTEGER"); addColumnIfNotExists("user_preferences", "hidden_rail_tabs", "TEXT"); + addColumnIfNotExists("user_preferences", "ai_assistant_enabled", "INTEGER"); + addColumnIfNotExists("user_preferences", "ai_read_only_commands", "INTEGER"); addColumnIfNotExists("user_preferences", "compact_host_view", "INTEGER"); addColumnIfNotExists("user_preferences", "status_color_scheme", "TEXT"); + addColumnIfNotExists("user_preferences", "custom_themes", "TEXT"); + addColumnIfNotExists("user_preferences", "custom_keybindings", "TEXT"); + addColumnIfNotExists("user_preferences", "terminal_defaults", "TEXT"); + addColumnIfNotExists("user_preferences", "rdp_defaults", "TEXT"); + addColumnIfNotExists("user_preferences", "terminal_macros", "TEXT"); sqlite.exec(` CREATE TABLE IF NOT EXISTS dashboard_service_links ( @@ -857,9 +990,7 @@ const migrateSchema = () => { databaseLogger.warn("Failed to backfill users.registered_at", { operation: "schema_migration", error: - backfillError instanceof Error - ? backfillError.message - : String(backfillError), + getErrorMessage(backfillError, String(backfillError)), }); } } else { @@ -873,9 +1004,7 @@ const migrateSchema = () => { { operation: "schema_migration", error: - backfillError instanceof Error - ? backfillError.message - : String(backfillError), + getErrorMessage(backfillError, String(backfillError)), }, ); } @@ -912,6 +1041,8 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_data", "folder", "TEXT"); addColumnIfNotExists("ssh_data", "tags", "TEXT"); addColumnIfNotExists("ssh_data", "pin", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists("ssh_data", "sort_order", "INTEGER"); + addColumnIfNotExists("ssh_folders", "sort_order", "INTEGER"); addColumnIfNotExists( "ssh_data", "auth_type", @@ -1002,11 +1133,22 @@ const migrateSchema = () => { "INTEGER NOT NULL DEFAULT 0", ); addColumnIfNotExists("ssh_data", "proxmox_config", "TEXT"); + addColumnIfNotExists( + "ssh_data", + "enable_proxmox_stats", + "INTEGER NOT NULL DEFAULT 0", + ); + addColumnIfNotExists("ssh_data", "proxmox_stats_config", "TEXT"); addColumnIfNotExists( "ssh_data", "enable_tmux_monitor", "INTEGER NOT NULL DEFAULT 0", ); + addColumnIfNotExists( + "ssh_data", + "enable_terminal_toolbar", + "INTEGER NOT NULL DEFAULT 1", + ); addColumnIfNotExists("ssh_data", "connection_type", 'TEXT NOT NULL DEFAULT "ssh"'); addColumnIfNotExists("ssh_data", "domain", "TEXT"); @@ -1061,6 +1203,13 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_credentials", "cert_public_key", "TEXT"); + addColumnIfNotExists( + "ssh_credentials", + "pin", + "INTEGER NOT NULL DEFAULT 0", + ); + addColumnIfNotExists("ssh_credentials", "sort_order", "INTEGER"); + try { const tableInfo = sqlite.prepare("PRAGMA table_info(ssh_credentials)").all() as Array<{ cid: number; @@ -1074,43 +1223,57 @@ const migrateSchema = () => { if (usernameCol && usernameCol.notnull === 1) { const tempTableName = "ssh_credentials_temp_migration"; - const allColumns = tableInfo.map((col) => col.name).join(", "); + const allColumns = tableInfo.map((col) => `"${col.name}"`).join(", "); + + // Derive the replacement table from the live definition instead of + // restating it here. The table keeps gaining columns (cert_public_key, + // pin, sort_order, sync_id, ...), and a second copy of the column list + // falls behind every time one is added โ€” leaving the copy narrower than + // the table, so the INSERT below fails and the constraint stays put. + const createSql = sqlite + .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'ssh_credentials'") + .pluck() + .get() as string | undefined; + + if (!createSql) { + throw new Error("ssh_credentials has no stored table definition"); + } + + // Only the table name is rewritten; replace() stops at the first match, + // and in a CREATE TABLE statement that is the table being defined. + const renamedSql = createSql.replace("ssh_credentials", tempTableName); + const tempCreateSql = renamedSql.replace(/(["`[]?username["`\]]?\s+TEXT)\s+NOT\s+NULL/i, "$1"); + + if (tempCreateSql === renamedSql) { + throw new Error("could not derive a nullable-username definition for ssh_credentials"); + } + + // DROP TABLE takes the table's indexes with it, so replay them afterwards. + const indexDefs = sqlite + .prepare( + "SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'ssh_credentials' AND sql IS NOT NULL", + ) + .pluck() + .all() as string[]; sqlite.exec(`PRAGMA foreign_keys = OFF`); sqlite.exec(` - CREATE TABLE ${tempTableName} ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - folder TEXT, - tags TEXT, - auth_type TEXT NOT NULL, - username TEXT, - password TEXT, - key TEXT, - key_password TEXT, - key_type TEXT, - usage_count INTEGER NOT NULL DEFAULT 0, - last_used TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - private_key TEXT, - public_key TEXT, - detected_key_type TEXT, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); + ${tempCreateSql}; - INSERT INTO ${tempTableName} SELECT ${allColumns} FROM ssh_credentials; + INSERT INTO ${tempTableName} (${allColumns}) SELECT ${allColumns} FROM ssh_credentials; DROP TABLE ssh_credentials; ALTER TABLE ${tempTableName} RENAME TO ssh_credentials; `); + for (const indexSql of indexDefs) { + sqlite.exec(indexSql); + } sqlite.exec(`PRAGMA foreign_keys = ON`); databaseLogger.info("Successfully migrated ssh_credentials table to remove username NOT NULL constraint", { operation: "schema_migration_username_nullable", + restoredIndexes: indexDefs.length, }); } } catch (migrationError) { @@ -1120,6 +1283,69 @@ const migrateSchema = () => { }); } + try { + const auditLogColumns = sqlite.prepare("PRAGMA table_info(audit_logs)").all() as Array<{ + name: string; + notnull: number; + }>; + const auditUserIdCol = auditLogColumns.find((col) => col.name === "user_id"); + + if (auditUserIdCol && auditUserIdCol.notnull === 1) { + const tempTableName = "audit_logs_temp_migration"; + const columns = [ + "id", + "user_id", + "username", + "action", + "resource_type", + "resource_id", + "resource_name", + "details", + "ip_address", + "user_agent", + "success", + "error_message", + "timestamp", + ].join(", "); + + sqlite.exec(`PRAGMA foreign_keys = OFF`); + sqlite.exec(` + CREATE TABLE ${tempTableName} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT, + username TEXT NOT NULL, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + resource_name TEXT, + details TEXT, + ip_address TEXT, + user_agent TEXT, + success INTEGER NOT NULL, + error_message TEXT, + timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL + ); + + INSERT INTO ${tempTableName} (${columns}) SELECT ${columns} FROM audit_logs; + + DROP TABLE audit_logs; + + ALTER TABLE ${tempTableName} RENAME TO audit_logs; + `); + sqlite.exec(`PRAGMA foreign_keys = ON`); + + databaseLogger.info("Successfully migrated audit_logs table to remove user_id NOT NULL constraint", { + operation: "schema_migration_audit_user_id_nullable", + }); + } + } catch (migrationError) { + databaseLogger.warn("Failed to migrate audit_logs user_id column", { + operation: "schema_migration", + error: migrationError, + }); + } + addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL"); addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL"); addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL"); @@ -1127,6 +1353,7 @@ const migrateSchema = () => { addColumnIfNotExists("snippets", "folder", "TEXT"); addColumnIfNotExists("snippets", "order", "INTEGER NOT NULL DEFAULT 0"); addColumnIfNotExists("snippets", "host_filter", "TEXT"); + addColumnIfNotExists("snippets", "is_note", "INTEGER NOT NULL DEFAULT 0"); try { sqlite @@ -1287,85 +1514,6 @@ const migrateSchema = () => { } } - try { - sqlite.prepare("SELECT id FROM c2s_tunnel_presets LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS c2s_tunnel_presets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - config TEXT NOT NULL, - platform TEXT, - computer_name TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create c2s_tunnel_presets table", { - operation: "schema_migration", - error: createError, - }); - } - } - - try { - sqlite - .prepare("SELECT id FROM sessions LIMIT 1") - .get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS sessions ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - jwt_token TEXT NOT NULL, - device_type TEXT NOT NULL, - device_info TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - last_active_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create sessions table", { - operation: "schema_migration", - error: createError, - }); - } - } - - try { - sqlite - .prepare("SELECT id FROM trusted_devices LIMIT 1") - .get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS trusted_devices ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - device_fingerprint TEXT NOT NULL, - device_type TEXT NOT NULL, - device_info TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create trusted_devices table", { - operation: "schema_migration", - error: createError, - }); - } - } - try { sqlite .prepare("SELECT id FROM network_topology LIMIT 1") @@ -1390,36 +1538,6 @@ const migrateSchema = () => { } } - try { - sqlite.prepare("SELECT id FROM host_access LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS host_access ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT, - role_id INTEGER, - granted_by TEXT NOT NULL, - permission_level TEXT NOT NULL DEFAULT 'use', - expires_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_accessed_at TEXT, - access_count INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE, - FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create host_access table", { - operation: "schema_migration", - error: createError, - }); - } - } - try { sqlite.prepare("SELECT role_id FROM host_access LIMIT 1").get(); } catch { @@ -1446,6 +1564,41 @@ const migrateSchema = () => { } } + try { + ensureSharedHostAuthOverrideProtocolSchema(sqlite); + } catch (schemaError) { + databaseLogger.warn("Failed to prepare shared_host_auth_overrides table", { + operation: "schema_migration", + error: schemaError, + }); + } + + try { + migrateLegacySharedHostAuthOverrides( + sqlite, + getRawSettingValue, + setRawSettingValue, + ); + } catch (migrateError) { + databaseLogger.warn("Failed to migrate shared host auth overrides", { + operation: "schema_migration", + error: migrateError, + }); + } + + try { + sqlite.prepare("SELECT credential_id FROM ssh_folders LIMIT 1").get(); + } catch { + try { + sqlite.exec("ALTER TABLE ssh_folders ADD COLUMN credential_id INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL"); + } catch (alterError) { + databaseLogger.warn("Failed to add credential_id column to ssh_folders", { + operation: "schema_migration", + error: alterError, + }); + } + } + try { sqlite.prepare("SELECT sudo_password FROM ssh_data LIMIT 1").get(); } catch { @@ -1463,6 +1616,7 @@ const migrateSchema = () => { { column: "connection_type", sql: "ALTER TABLE ssh_data ADD COLUMN connection_type TEXT NOT NULL DEFAULT 'ssh'" }, { column: "credential_id", sql: "ALTER TABLE ssh_data ADD COLUMN credential_id INTEGER" }, { column: "override_credential_username", sql: "ALTER TABLE ssh_data ADD COLUMN override_credential_username INTEGER" }, + { column: "share_ssh_auth", sql: "ALTER TABLE ssh_data ADD COLUMN share_ssh_auth INTEGER NOT NULL DEFAULT 0" }, { column: "jump_hosts", sql: "ALTER TABLE ssh_data ADD COLUMN jump_hosts TEXT" }, { column: "show_terminal_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1" }, { column: "show_file_manager_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0" }, @@ -1508,6 +1662,9 @@ const migrateSchema = () => { { column: "rdp_auth_type", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_auth_type TEXT" }, { column: "vnc_auth_type", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_auth_type TEXT" }, { column: "telnet_auth_type", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_auth_type TEXT" }, + { column: "allow_session_sharing", sql: "ALTER TABLE ssh_data ADD COLUMN allow_session_sharing INTEGER NOT NULL DEFAULT 1" }, + { column: "connection_origin", sql: "ALTER TABLE ssh_data ADD COLUMN connection_origin TEXT" }, + { column: "parent_host_id", sql: "ALTER TABLE ssh_data ADD COLUMN parent_host_id INTEGER REFERENCES ssh_data(id) ON DELETE SET NULL" }, ]; for (const migration of sshDataMigrations) { @@ -1525,6 +1682,40 @@ const migrateSchema = () => { } } + // share_ssh_auth arrived with 2.6.1 and defaults to 0, but sharing a host + // used to pass the owner's SSH authentication along unconditionally. Every + // host shared before the upgrade therefore stopped supplying credentials to + // its recipients the moment the column appeared, and they were left with + // "No valid authentication method provided". + // + // Turn it on for hosts that are already shared, which is where the previous + // behaviour was in effect and consented to. Hosts nobody has shared keep the + // new default; the owner decides when they share one. + try { + if (getRawSettingValue("share_ssh_auth_backfill_v1") === null) { + const backfilled = sqlite + .prepare( + `UPDATE ssh_data SET share_ssh_auth = 1 + WHERE share_ssh_auth = 0 + AND id IN (SELECT DISTINCT host_id FROM host_access)`, + ) + .run(); + + if (backfilled.changes > 0) { + databaseLogger.info( + `Restored shared SSH authentication for ${backfilled.changes} already-shared host(s)`, + { operation: "share_ssh_auth_backfill_v1" }, + ); + } + setRawSettingValue("share_ssh_auth_backfill_v1", "true"); + } + } catch (e) { + databaseLogger.warn("Failed to backfill share_ssh_auth", { + operation: "share_ssh_auth_backfill_v1", + error: e, + }); + } + // Migrate legacy authType="warpgate" hosts to useWarpgate=1 with authType="none" try { const result = sqlite @@ -1594,118 +1785,6 @@ const migrateSchema = () => { // user_open_tabs table not present yet; nothing to migrate. } - try { - sqlite.prepare("SELECT id FROM roles LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - display_name TEXT NOT NULL, - description TEXT, - is_system INTEGER NOT NULL DEFAULT 0, - permissions TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create roles table", { - operation: "schema_migration", - error: createError, - }); - } - } - - try { - sqlite.prepare("SELECT id FROM user_roles LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS user_roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - role_id INTEGER NOT NULL, - granted_by TEXT, - granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, role_id), - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE, - FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create user_roles table", { - operation: "schema_migration", - error: createError, - }); - } - } - - try { - sqlite.prepare("SELECT id FROM audit_logs LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS audit_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - username TEXT NOT NULL, - action TEXT NOT NULL, - resource_type TEXT NOT NULL, - resource_id TEXT, - resource_name TEXT, - details TEXT, - ip_address TEXT, - user_agent TEXT, - success INTEGER NOT NULL, - error_message TEXT, - timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create audit_logs table", { - operation: "schema_migration", - error: createError, - }); - } - } - - try { - sqlite.prepare("SELECT id FROM session_recordings LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS session_recordings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT NOT NULL, - access_id INTEGER, - started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - ended_at TEXT, - duration INTEGER, - commands TEXT, - dangerous_actions TEXT, - recording_path TEXT, - protocol TEXT NOT NULL DEFAULT 'ssh', - format TEXT NOT NULL DEFAULT 'text', - terminated_by_owner INTEGER DEFAULT 0, - termination_reason TEXT, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create session_recordings table", { - operation: "schema_migration", - error: createError, - }); - } - } - try { sqlite.prepare("SELECT id FROM shared_host_secrets LIMIT 1").get(); } catch { @@ -1847,32 +1926,6 @@ const migrateSchema = () => { } } - try { - sqlite.prepare("SELECT id FROM api_keys LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS api_keys ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - token_hash TEXT NOT NULL, - token_prefix TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT, - last_used_at TEXT, - is_active INTEGER NOT NULL DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); - `); - } catch (createError) { - databaseLogger.warn("Failed to create api_keys table", { - operation: "schema_migration", - error: createError, - }); - } - } - // --- tmux-monitor begin --- try { sqlite.prepare("SELECT id FROM tmux_session_tags LIMIT 1").get(); @@ -2053,6 +2106,74 @@ const migrateSchema = () => { addColumnIfNotExists("users", "sso_provider_id", "INTEGER"); + try { + const usersTableInfo = sqlite.prepare("PRAGMA table_info(users)").all() as Array<{ + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; + }>; + const legacyNotNullColumns = new Set([ + "client_id", + "client_secret", + "issuer_url", + "authorization_url", + "token_url", + "identifier_path", + "name_path", + "scopes", + ]); + const hasStaleNotNull = usersTableInfo.some( + (col) => legacyNotNullColumns.has(col.name) && col.notnull === 1, + ); + + if (hasStaleNotNull) { + const tempTableName = "users_temp_migration"; + const columnDefs = usersTableInfo + .map((col) => { + const parts = [`"${col.name}"`, col.type || "TEXT"]; + if (col.pk === 1) parts.push("PRIMARY KEY"); + if (col.notnull === 1 && !legacyNotNullColumns.has(col.name)) { + parts.push("NOT NULL"); + } + if (col.dflt_value !== null) { + parts.push(`DEFAULT ${col.dflt_value}`); + } + return parts.join(" "); + }) + .join(",\n "); + const allColumns = usersTableInfo.map((col) => `"${col.name}"`).join(", "); + + sqlite.exec(`PRAGMA foreign_keys = OFF`); + sqlite.exec(` + CREATE TABLE ${tempTableName} ( + ${columnDefs} + ); + + INSERT INTO ${tempTableName} SELECT ${allColumns} FROM users; + + DROP TABLE users; + + ALTER TABLE ${tempTableName} RENAME TO users; + `); + sqlite.exec(`PRAGMA foreign_keys = ON`); + + databaseLogger.info( + "Successfully migrated users table to remove legacy OIDC NOT NULL constraints", + { + operation: "schema_migration_users_oidc_nullable", + }, + ); + } + } catch (migrationError) { + databaseLogger.warn("Failed to migrate users table legacy OIDC columns", { + operation: "schema_migration", + error: migrationError, + }); + } + // Migrate legacy single oidc_config settings blob into sso_providers table try { const migrationDone = getRawSettingValue("sso_migration_v1"); @@ -2115,6 +2236,34 @@ const migrateSchema = () => { } // --- metrics-history end --- + // --- proxmox-node-history begin --- + try { + sqlite.prepare("SELECT id FROM proxmox_node_history LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS proxmox_node_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE, + ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + cpu_percent REAL, + mem_percent REAL, + disk_percent REAL, + net_rx_bytes INTEGER, + net_tx_bytes INTEGER + ); + CREATE INDEX IF NOT EXISTS idx_proxmox_node_history_host_ts + ON proxmox_node_history (host_id, ts DESC); + `); + } catch (createError) { + databaseLogger.warn("Failed to create proxmox_node_history table", { + operation: "schema_migration", + error: createError, + }); + } + } + // --- proxmox-node-history end --- + // --- alerts begin --- try { sqlite.prepare("SELECT id FROM alert_rules LIMIT 1").get(); @@ -2215,6 +2364,159 @@ const migrateSchema = () => { } // --- alerts end --- + // --- automations begin --- + try { + sqlite.prepare("SELECT id FROM automations LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS automations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + definition TEXT NOT NULL, + definition_version INTEGER NOT NULL DEFAULT 1, + concurrency_policy TEXT NOT NULL DEFAULT 'skip', + max_run_seconds INTEGER NOT NULL DEFAULT 300, + dry_run INTEGER NOT NULL DEFAULT 0, + last_run_at TEXT, + last_run_status TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create automations table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM automation_trigger_state LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS automation_trigger_state ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE, + state_key TEXT NOT NULL, + breach_started_at TEXT, + last_fired_at TEXT, + last_value REAL, + last_observed_state TEXT, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create automation_trigger_state table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM automation_schedules LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS automation_schedules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE, + cron TEXT, + interval_seconds INTEGER, + timezone TEXT, + next_due_at TEXT, + last_tick_at TEXT + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create automation_schedules table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM automation_runs LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS automation_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + trigger_type TEXT NOT NULL, + trigger_context TEXT, + status TEXT NOT NULL, + started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + finished_at TEXT, + duration_ms INTEGER, + error TEXT, + dry_run INTEGER NOT NULL DEFAULT 0, + parent_run_id INTEGER + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create automation_runs table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM automation_run_steps LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS automation_run_steps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id INTEGER NOT NULL REFERENCES automation_runs(id) ON DELETE CASCADE, + step_index INTEGER NOT NULL, + step_id TEXT NOT NULL, + step_type TEXT NOT NULL, + status TEXT NOT NULL, + started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + finished_at TEXT, + output TEXT, + error TEXT, + truncated INTEGER NOT NULL DEFAULT 0 + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create automation_run_steps table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM automation_channels LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS automation_channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE, + channel_id INTEGER NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create automation_channels table", { + operation: "schema_migration", + error: createError, + }); + } + } + // --- automations end --- + // Seed default metrics history retention setting try { ensureRawSettingDefault("metrics_history_retention_days", "7"); @@ -2274,11 +2576,368 @@ const migrateSchema = () => { } // --- homepage end --- + // --- fleets begin --- + try { + sqlite.prepare("SELECT id FROM fleets LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS fleets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + color TEXT, + icon TEXT, + tag_rules TEXT, + sync_id TEXT UNIQUE, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create fleets table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM fleet_members LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS fleet_members ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fleet_id INTEGER NOT NULL REFERENCES fleets(id) ON DELETE CASCADE, + host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE, + added_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + sqlite.exec( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_fleet_members_fleet_host ON fleet_members(fleet_id, host_id)", + ); + } catch (createError) { + databaseLogger.warn("Failed to create fleet_members table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM fleet_inventory LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS fleet_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + os_pretty_name TEXT, + kernel TEXT, + architecture TEXT, + hostname TEXT, + uptime_seconds INTEGER, + ip TEXT, + package_manager TEXT, + collected_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + sqlite.exec( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_fleet_inventory_host ON fleet_inventory(host_id, user_id)", + ); + } catch (createError) { + databaseLogger.warn("Failed to create fleet_inventory table", { + operation: "schema_migration", + error: createError, + }); + } + } + // --- fleets end --- + + // --- workspaces begin --- + try { + sqlite.prepare("SELECT id FROM user_workspaces LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS user_workspaces ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + color TEXT, + icon TEXT, + kind TEXT NOT NULL DEFAULT 'manual', + is_default INTEGER NOT NULL DEFAULT 0, + payload TEXT NOT NULL DEFAULT '{}', + sync_id TEXT UNIQUE, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_used_at TEXT + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create user_workspaces table", { + operation: "schema_migration", + error: createError, + }); + } + } + // --- workspaces end --- + + // --- sync begin --- + // Stable per-row identity used to match rows across two independently- + // seeded databases (the embedded desktop backend and a connected remote + // server) during sync. Local autoincrement ids collide across instances, + // so a randomly-generated id is the join key instead. SQLite refuses a + // non-constant DEFAULT (e.g. randomblob()) on ALTER TABLE ADD COLUMN for + // tables with existing constraints ("Cannot add a column with + // non-constant default"), so the column is added as plain nullable TEXT; + // repositories set syncId explicitly on insert going forward, and + // existing rows are backfilled by the UPDATE loop below. + addColumnIfNotExists("ssh_data", "sync_id", "TEXT"); + addColumnIfNotExists("ssh_credentials", "sync_id", "TEXT"); + addColumnIfNotExists("ssh_folders", "sync_id", "TEXT"); + addColumnIfNotExists("snippets", "sync_id", "TEXT"); + addColumnIfNotExists("snippet_folders", "sync_id", "TEXT"); + addColumnIfNotExists("vault_profiles", "sync_id", "TEXT"); + addColumnIfNotExists("dashboard_service_links", "sync_id", "TEXT"); + // SQLite also rejects NOT NULL DEFAULT CURRENT_TIMESTAMP here for the same + // "non-constant default" reason -- add nullable, then backfill from + // created_at below and rely on the repository layer to keep it current. + addColumnIfNotExists("dashboard_service_links", "updated_at", "TEXT"); + try { + sqlite.exec( + "UPDATE dashboard_service_links SET updated_at = created_at WHERE updated_at IS NULL", + ); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + databaseLogger.warn( + `Failed to backfill dashboard_service_links.updated_at: ${message}`, + { operation: "schema_migration", table: "dashboard_service_links" }, + ); + } + addColumnIfNotExists("homepage_items", "sync_id", "TEXT"); + + const syncIdTables = [ + "ssh_data", + "ssh_credentials", + "ssh_folders", + "snippets", + "snippet_folders", + "vault_profiles", + "dashboard_service_links", + "homepage_items", + ]; + + for (const table of syncIdTables) { + try { + const result = sqlite + .prepare( + `UPDATE ${table} SET sync_id = lower(hex(randomblob(16))) WHERE sync_id IS NULL`, + ) + .run(); + if (result.changes > 0) { + databaseLogger.info( + `Backfilled sync_id for ${result.changes} row(s) in ${table}`, + { operation: "sync_id_backfill", table }, + ); + } + sqlite.exec( + `CREATE UNIQUE INDEX IF NOT EXISTS idx_${table}_sync_id ON ${table}(sync_id)`, + ); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + databaseLogger.warn( + `Failed to backfill sync_id for ${table}: ${message}`, + { + operation: "sync_id_backfill", + table, + }, + ); + } + } + + try { + sqlite.prepare("SELECT id FROM sync_tombstones LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS sync_tombstones ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + entity_type TEXT NOT NULL, + sync_id TEXT NOT NULL, + deleted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + sqlite.exec( + "CREATE INDEX IF NOT EXISTS idx_sync_tombstones_user_entity ON sync_tombstones(user_id, entity_type)", + ); + } catch (createError) { + databaseLogger.warn("Failed to create sync_tombstones table", { + operation: "schema_migration", + error: createError, + }); + } + } + // --- sync end --- + + // --- ai begin --- + try { + sqlite.prepare("SELECT id FROM ai_providers LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS ai_providers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider_type TEXT NOT NULL, + label TEXT NOT NULL, + base_url TEXT, + api_key TEXT, + api_key_prefix TEXT, + default_model TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, label) + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create ai_providers table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM ai_conversations LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS ai_conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title TEXT, + provider_id INTEGER, + model TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create ai_conversations table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM ai_messages LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS ai_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id INTEGER NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE, + role TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + tool_calls TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create ai_messages table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM ai_proposals LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS ai_proposals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id INTEGER NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + kind TEXT NOT NULL, + summary TEXT, + payload TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'pending', + applied_at TEXT, + result_summary TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create ai_proposals table", { + operation: "schema_migration", + error: createError, + }); + } + } + + // --- ai end --- + + // Audit trails and session recordings used to be deleted along with the user + // they referenced, which defeats the point of keeping them. + migrateAuditRetention(sqlite); + + // Runs last so every table and column added above already exists. + createPerformanceIndexes(sqlite); + databaseLogger.success("Schema migration completed", { operation: "schema_migration", }); }; +// A trivial telemetry write forces `serialize()` to rewrite every free page +// along with the live ones, so a database that has accumulated a large +// freelist (from years of unbounded metrics/audit growth before retention +// pruning existed) turns every future save into a multi-megabyte rewrite. +// Reclaiming that space once at startup keeps steady-state saves cheap. +const VACUUM_FREELIST_COUNT_THRESHOLD = 2000; +const VACUUM_FREELIST_RATIO_THRESHOLD = 0.5; + +function vacuumIfFreelistBloated(): void { + try { + const pageCount = sqlite.pragma("page_count", { simple: true }) as number; + const freelistCount = sqlite.pragma("freelist_count", { + simple: true, + }) as number; + if (pageCount <= 0) return; + + const freelistRatio = freelistCount / pageCount; + if ( + freelistCount < VACUUM_FREELIST_COUNT_THRESHOLD || + freelistRatio < VACUUM_FREELIST_RATIO_THRESHOLD + ) { + return; + } + + databaseLogger.info("Reclaiming bloated SQLite freelist on startup", { + operation: "db_startup_vacuum", + pageCount, + freelistCount, + freelistRatio, + }); + sqlite.exec("VACUUM"); + } catch (error) { + databaseLogger.warn("Failed to vacuum database on startup", { + operation: "db_startup_vacuum_failed", + error: error instanceof Error ? error.message : String(error), + }); + } +} + // Callers here do not coordinate with each other, and only DatabaseSaveTrigger // tracks whether a save is already running. Overlapping saves can write an // older snapshot last, so run them in order. @@ -2350,9 +3009,7 @@ async function handlePostInitFileEncryption() { databaseLogger.warn("Failed to cleanup old migration files", { operation: "migration_cleanup_startup_failed", error: - cleanupError instanceof Error - ? cleanupError.message - : "Unknown error", + getErrorMessage(cleanupError), }); } } catch (error) { @@ -2367,10 +3024,54 @@ async function handlePostInitFileEncryption() { } async function initializeDatabase(): Promise { + const dialect = resolveDatabaseDialect(); + + if (dialect !== "sqlite") { + await initializeRemoteDatabase(dialect); + return; + } + await initializeCompleteDatabase(); await handlePostInitFileEncryption(); } +/** + * Startup against Postgres or MySQL. + * + * Shorter than the SQLite path because most of what that one does has no + * counterpart here: there is no file to decrypt, no in-memory copy to keep in + * step with disk, and the schema comes from drizzle-kit migrations instead of + * the inline DDL below. + * + * What does carry over is the settings cache. 27 call sites read settings + * synchronously, which better-sqlite3 allows and no remote driver does, so the + * table is loaded once here before anything asks for it. + */ +async function initializeRemoteDatabase( + dialect: Exclude, +): Promise { + databaseLogger.info(`Connecting to ${dialect} database`, { + operation: "db_init", + dialect, + }); + + db = await connectRemoteDatabase(dialect); + await runRemoteMigrations(dialect, db); + + // Imported here rather than at the top: factory.ts imports getDb from this + // module, and a static import would close the cycle at module-load time. + const { primeCurrentSettingsCache, startSettingsCacheRefresh } = await import( + "../repositories/factory.js" + ); + await primeCurrentSettingsCache(); + startSettingsCacheRefresh(); + + databaseLogger.info(`${dialect} database ready`, { + operation: "db_init_complete", + dialect, + }); +} + export { initializeDatabase }; async function cleanupDatabase() { @@ -2395,7 +3096,7 @@ async function cleanupDatabase() { } catch (error) { databaseLogger.warn("Error closing database connection", { operation: "db_close_error", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } @@ -2448,9 +3149,9 @@ process.on("SIGTERM", async () => { process.exit(0); }); -let db: ReturnType>; +let db: PortableDatabase; -export function getDb(): ReturnType> { +export function getDb(): PortableDatabase { if (!db) { throw new Error( "Database not initialized. Ensure initializeDatabase() is called before accessing db.", @@ -2461,6 +3162,13 @@ export function getDb(): ReturnType> { export function getSqlite(): Database.Database { if (!sqlite) { + const dialect = resolveDatabaseDialect(); + if (dialect !== "sqlite") { + throw new Error( + `No SQLite handle: DATABASE_DIALECT is "${dialect}". This caller needs a ` + + `synchronous query, which only SQLite offers โ€” give it an async path instead.`, + ); + } throw new Error( "SQLite not initialized. Ensure initializeDatabase() is called before accessing sqlite.", ); diff --git a/src/backend/database/db/migrate.ts b/src/backend/database/db/migrate.ts new file mode 100644 index 0000000..57d9991 --- /dev/null +++ b/src/backend/database/db/migrate.ts @@ -0,0 +1,51 @@ +import path from "path"; +import type { DatabaseDialect } from "./dialect.js"; +import type { PortableDatabase } from "../repositories/database-context.js"; + +export const MIGRATIONS_DIR_ENV = "DRIZZLE_MIGRATIONS_DIR"; + +/** + * Where the generated migrations live. + * + * SQLite does not appear here: it builds its schema from the DDL in index.ts + * and patches it forward with migrateSchema(). Only the client-server engines + * use drizzle-kit migrations, and each has its own folder because the + * generated SQL differs per dialect. + */ +export function migrationsFolder( + dialect: DatabaseDialect, + env: NodeJS.ProcessEnv = process.env, +): string { + const override = env[MIGRATIONS_DIR_ENV]?.trim(); + const root = override || path.resolve(process.cwd(), "drizzle"); + return path.join(root, dialect); +} + +/** + * Brings a remote database up to the current schema. + * + * drizzle's migrator records what it has applied in its own table, so this is + * safe to run on every start โ€” including against a database another instance + * already migrated. + */ +export async function runRemoteMigrations( + dialect: DatabaseDialect, + db: PortableDatabase, + env: NodeJS.ProcessEnv = process.env, +): Promise { + if (dialect === "sqlite") { + throw new Error("SQLite builds its schema in index.ts, not from drizzle/"); + } + + const folder = migrationsFolder(dialect, env); + + const { migrate } = + dialect === "postgres" + ? await import("drizzle-orm/node-postgres/migrator") + : await import("drizzle-orm/mysql2/migrator"); + + await (migrate as (db: unknown, config: { migrationsFolder: string }) => Promise)( + db, + { migrationsFolder: folder }, + ); +} diff --git a/src/backend/database/db/performance-indexes.ts b/src/backend/database/db/performance-indexes.ts new file mode 100644 index 0000000..2e1ca83 --- /dev/null +++ b/src/backend/database/db/performance-indexes.ts @@ -0,0 +1,352 @@ +import { databaseLogger } from "../../utils/logger.js"; + +/** + * Indexes for the columns the app filters, joins and sorts on. + * + * Most tables here are scoped per user or per host and were previously reached + * with a full table scan on every read: the foreign keys existed, but SQLite + * does not index the child side of a foreign key on its own. That is fine for a + * home install with a handful of hosts and invisible in testing; on an install + * with thousands of hosts and a large audit trail it is the dominant cost of a + * request. + * + * Ordering within a composite matters: the equality column comes first and the + * range/sort column second, so the same index serves both the filter and the + * ORDER BY. + */ +export interface PerformanceIndex { + name: string; + table: string; + columns: string; + /** Enforces a constraint as well as serving reads, e.g. one row per key. */ + unique?: boolean; +} + +export const PERFORMANCE_INDEXES: PerformanceIndex[] = [ + // Host list: the single hottest read in the app. + { name: "idx_ssh_data_user_id", table: "ssh_data", columns: "user_id" }, + { + name: "idx_ssh_data_parent_host", + table: "ssh_data", + columns: "parent_host_id", + }, + { + name: "idx_ssh_data_credential", + table: "ssh_data", + columns: "credential_id", + }, + + // Sharing: resolved for every host list request and every permission check. + { name: "idx_host_access_user_id", table: "host_access", columns: "user_id" }, + { name: "idx_host_access_role_id", table: "host_access", columns: "role_id" }, + { name: "idx_host_access_host_id", table: "host_access", columns: "host_id" }, + { + name: "idx_host_access_expires_at", + table: "host_access", + columns: "expires_at", + }, + + // Audit log: grows without bound and is always read newest-first. + { + name: "idx_audit_logs_timestamp", + table: "audit_logs", + columns: "timestamp", + }, + { + name: "idx_audit_logs_user_ts", + table: "audit_logs", + columns: "user_id, timestamp", + }, + { + name: "idx_audit_logs_action_ts", + table: "audit_logs", + columns: "action, timestamp", + }, + { + name: "idx_audit_logs_resource_ts", + table: "audit_logs", + columns: "resource_type, timestamp", + }, + + // Auth hot path: touched on every authenticated request. + { name: "idx_sessions_user_id", table: "sessions", columns: "user_id" }, + { name: "idx_sessions_expires_at", table: "sessions", columns: "expires_at" }, + { name: "idx_user_roles_user_id", table: "user_roles", columns: "user_id" }, + { name: "idx_user_roles_role_id", table: "user_roles", columns: "role_id" }, + { + name: "idx_trusted_devices_user_id", + table: "trusted_devices", + columns: "user_id", + }, + { name: "idx_api_keys_user_id", table: "api_keys", columns: "user_id" }, + + // Credentials and folders. + { + name: "idx_ssh_credentials_user_id", + table: "ssh_credentials", + columns: "user_id", + }, + { name: "idx_ssh_folders_user_id", table: "ssh_folders", columns: "user_id" }, + { + name: "idx_ssh_credential_usage_credential", + table: "ssh_credential_usage", + columns: "credential_id", + }, + { + name: "idx_ssh_credential_usage_user", + table: "ssh_credential_usage", + columns: "user_id", + }, + + // Snippets. + { name: "idx_snippets_user_id", table: "snippets", columns: "user_id" }, + { + name: "idx_snippet_access_user_id", + table: "snippet_access", + columns: "user_id", + }, + { + name: "idx_snippet_access_snippet_id", + table: "snippet_access", + columns: "snippet_id", + }, + { + name: "idx_snippet_access_role_id", + table: "snippet_access", + columns: "role_id", + }, + + // Per-user history and file manager surfaces. + { + name: "idx_recent_activity_user_ts", + table: "recent_activity", + columns: "user_id, timestamp", + }, + { + name: "idx_command_history_user_host", + table: "command_history", + columns: "user_id, host_id", + }, + { + name: "idx_file_manager_recent_user", + table: "file_manager_recent", + columns: "user_id, host_id", + }, + { + name: "idx_file_manager_pinned_user", + table: "file_manager_pinned", + columns: "user_id, host_id", + }, + { + name: "idx_file_manager_shortcuts_user", + table: "file_manager_shortcuts", + columns: "user_id, host_id", + }, + { + name: "idx_transfer_recent_user", + table: "transfer_recent", + columns: "user_id", + }, + { + name: "idx_user_open_tabs_user_id", + table: "user_open_tabs", + columns: "user_id", + }, + { + name: "idx_user_workspaces_user_id", + table: "user_workspaces", + columns: "user_id", + }, + { + name: "idx_homepage_items_user_id", + table: "homepage_items", + columns: "user_id", + }, + { + name: "idx_dismissed_alerts_user_id", + table: "dismissed_alerts", + columns: "user_id", + }, + + // Recordings and live session sharing. + { + name: "idx_session_recordings_user_started", + table: "session_recordings", + columns: "user_id, started_at", + }, + { + name: "idx_session_recordings_host", + table: "session_recordings", + columns: "host_id", + }, + { + name: "idx_session_shares_session_id", + table: "session_shares", + columns: "session_id", + }, + { + name: "idx_session_shares_host_id", + table: "session_shares", + columns: "host_id", + }, + + // Fleets. + { + name: "idx_fleet_members_fleet", + table: "fleet_members", + columns: "fleet_id", + }, + { + name: "idx_fleet_members_host", + table: "fleet_members", + columns: "host_id", + }, + { + name: "idx_fleet_inventory_user", + table: "fleet_inventory", + columns: "user_id", + }, + + // Alerting. + { + name: "idx_alert_firings_rule", + table: "alert_firings", + columns: "rule_id, fired_at", + }, + { + name: "idx_alert_firings_host", + table: "alert_firings", + columns: "host_id", + }, + + // Automations. + { + name: "idx_automations_user", + table: "automations", + columns: "user_id, enabled", + }, + { + name: "idx_automation_trigger_state_key", + table: "automation_trigger_state", + columns: "automation_id, state_key", + unique: true, + }, + { + name: "idx_automation_schedules_automation", + table: "automation_schedules", + columns: "automation_id", + unique: true, + }, + { + name: "idx_automation_schedules_due", + table: "automation_schedules", + columns: "next_due_at", + }, + { + name: "idx_automation_runs_automation", + table: "automation_runs", + columns: "automation_id, started_at", + }, + { + name: "idx_automation_runs_user", + table: "automation_runs", + columns: "user_id, started_at", + }, + { + name: "idx_automation_run_steps_run", + table: "automation_run_steps", + columns: "run_id, step_index", + }, + { + name: "idx_automation_channels_pair", + table: "automation_channels", + columns: "automation_id, channel_id", + unique: true, + }, + + // AI assistant. + { + name: "idx_ai_providers_user_label", + table: "ai_providers", + columns: "user_id, label", + unique: true, + }, + { + name: "idx_ai_conversations_user", + table: "ai_conversations", + columns: "user_id, updated_at", + }, + { + name: "idx_ai_messages_conversation", + table: "ai_messages", + columns: "conversation_id, created_at", + }, + { + name: "idx_ai_proposals_user", + table: "ai_proposals", + columns: "user_id, status", + }, + { + name: "idx_ai_proposals_conversation", + table: "ai_proposals", + columns: "conversation_id", + }, +]; + +interface IndexableDatabase { + exec(sql: string): unknown; +} + +export interface IndexCreationSummary { + created: number; + skipped: number; + failed: number; +} + +/** + * Creates any missing index, leaving existing ones untouched. + * + * A failure here is logged and skipped rather than thrown: an index is a pure + * optimisation, and a table that a given install has not created yet (or a + * column an older schema is missing) must not stop the server from booting. + */ +export function createPerformanceIndexes( + db: IndexableDatabase, + indexes: PerformanceIndex[] = PERFORMANCE_INDEXES, +): IndexCreationSummary { + const summary: IndexCreationSummary = { created: 0, skipped: 0, failed: 0 }; + const startedAt = Date.now(); + + for (const index of indexes) { + try { + db.exec( + `CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${index.name} ON ${index.table}(${index.columns})`, + ); + summary.created++; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + // A missing table or column means this install does not have that + // feature's schema; nothing to index and nothing to warn loudly about. + if (/no such table|no such column/i.test(message)) { + summary.skipped++; + continue; + } + summary.failed++; + databaseLogger.warn(`Could not create index ${index.name}: ${message}`, { + operation: "performance_index_create", + index: index.name, + table: index.table, + }); + } + } + + databaseLogger.info( + `Performance indexes ready in ${Date.now() - startedAt}ms`, + { + operation: "performance_index_create", + ...summary, + }, + ); + + return summary; +} diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts new file mode 100644 index 0000000..97253eb --- /dev/null +++ b/src/backend/database/db/schema.mysql.ts @@ -0,0 +1,1941 @@ +// GENERATED FILE โ€” do not edit. +// +// Produced from schema.ts by scripts/generate-dialect-schema.cjs. +// Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`. +// Target dialect: mysql. +// +// DDL source for drizzle-kit. NOT imported to run queries โ€” repositories use +// schema.ts on every dialect. See the generator header for why that is correct. + +import { + mysqlTable, + text, + varchar, + int, + boolean, + double, + index, + uniqueIndex, + type AnyMySqlColumn, +} from "drizzle-orm/mysql-core"; +import { sql } from "drizzle-orm"; + +export const users = mysqlTable("users", { + id: varchar("id", { length: 255 }).primaryKey(), + username: text("username").notNull(), + passwordHash: text("password_hash").notNull(), + isAdmin: boolean("is_admin").notNull().default(false), + + isOidc: boolean("is_oidc").notNull().default(false), + oidcIdentifier: text("oidc_identifier"), + ssoProviderId: int("sso_provider_id"), + clientId: text("client_id"), + clientSecret: text("client_secret"), + issuerUrl: text("issuer_url"), + authorizationUrl: text("authorization_url"), + tokenUrl: text("token_url"), + identifierPath: text("identifier_path"), + namePath: text("name_path"), + scopes: text().default("openid email profile"), + + totpSecret: text("totp_secret"), + totpEnabled: boolean("totp_enabled") + .notNull() + .default(false), + totpBackupCodes: text("totp_backup_codes"), + + registeredAt: text("registered_at").notNull().default(sql`(CURRENT_TIMESTAMP)`), + donationModalDismissed: boolean("donation_modal_dismissed") + .notNull() + .default(false), +}); + +export const settings = mysqlTable("settings", { + key: varchar("key", { length: 255 }).primaryKey(), + value: text("value").notNull(), +}); + +export const ssoProviders = mysqlTable("sso_providers", { + id: int("id").autoincrement().primaryKey(), + name: varchar("name", { length: 255 }).notNull(), + type: text("type").notNull(), + enabled: boolean("enabled").notNull().default(true), + displayOrder: int("display_order").notNull().default(0), + config: text("config").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const sessions = mysqlTable( + "sessions", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + jwtToken: text("jwt_token").notNull(), + deviceType: text("device_type").notNull(), + deviceInfo: text("device_info").notNull(), + oidcSub: text("oidc_sub"), + oidcSid: text("oidc_sid"), + ssoProviderId: int("sso_provider_id"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastActiveAt: text("last_active_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Listing a user's devices, and the startup sweep of expired rows. + (table) => [ + index("idx_sessions_user_id").on(table.userId), + index("idx_sessions_expires_at").on(table.expiresAt), + ], +); + +export const trustedDevices = mysqlTable( + "trusted_devices", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + deviceFingerprint: text("device_fingerprint").notNull(), + deviceType: text("device_type").notNull(), + deviceInfo: text("device_info").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastUsedAt: text("last_used_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [index("idx_trusted_devices_user_id").on(table.userId)], +); + +export const webauthnCredentials = mysqlTable("webauthn_credentials", { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + credentialId: varchar("credential_id", { length: 255 }).notNull(), + publicKey: text("public_key").notNull(), + counter: int("counter").notNull().default(0), + deviceType: text("device_type"), + backedUp: boolean("backed_up").notNull().default(false), + transports: text("transports"), + userVerification: text("user_verification").notNull().default("preferred"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + lastUsedAt: text("last_used_at"), +}); + +export const hosts = mysqlTable( + "ssh_data", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + connectionType: text("connection_type").notNull().default("ssh"), + name: varchar("name", { length: 255 }), + ip: text("ip").notNull(), + port: int("port").notNull(), + username: text("username").notNull(), + folder: text("folder"), + // Sub-host nesting: a host acting as an organizational parent for other + // hosts, mutually exclusive with folder (see host route validation). + parentHostId: int("parent_host_id").references( + (): AnyMySqlColumn => hosts.id, + { onDelete: "set null" }, + ), + tags: text("tags"), + pin: boolean("pin").notNull().default(false), + // Manual drag-to-reorder position within a folder. Null means the host has + // never been manually reordered; falls back to name sort in that case. + sortOrder: int("sort_order"), + authType: text("auth_type").notNull(), + useWarpgate: boolean("use_warpgate").notNull().default(false), + shareSshAuth: boolean("share_ssh_auth") + .notNull() + .default(false), + forceKeyboardInteractive: text("force_keyboard_interactive"), + + password: text("password"), + key: text("key"), + keyPassword: text("key_password"), + keyType: text("key_type"), + sudoPassword: text("sudo_password"), + + autostartPassword: text("autostart_password"), + autostartKey: text("autostart_key"), + autostartKeyPassword: text("autostart_key_password"), + + credentialId: int("credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + overrideCredentialUsername: boolean("override_credential_username"), + // When authType is "vault", the host authenticates via a Vault SSH signer + // profile (shared settings, no secrets). The signing certificate is obtained + // per-user at connect time via an interactive Vault OIDC flow. + vaultProfileId: int("vault_profile_id").references( + () => vaultProfiles.id, + { onDelete: "set null" }, + ), + enableTerminal: boolean("enable_terminal") + .notNull() + .default(true), + enableSessionLogging: boolean("enable_session_logging") + .notNull() + .default(true), + allowSessionSharing: boolean("allow_session_sharing") + .notNull() + .default(true), + enableCommandHistory: boolean("enable_command_history") + .notNull() + .default(true), + enableTunnel: boolean("enable_tunnel") + .notNull() + .default(true), + tunnelConnections: text("tunnel_connections"), + jumpHosts: text("jump_hosts"), + enableFileManager: boolean("enable_file_manager") + .notNull() + .default(true), + scpLegacy: boolean("scp_legacy").notNull().default(false), + enableDocker: boolean("enable_docker") + .notNull() + .default(false), + enableTmuxMonitor: boolean("enable_tmux_monitor") + .notNull() + .default(false), + enableTerminalToolbar: boolean("enable_terminal_toolbar") + .notNull() + .default(true), + showTerminalInSidebar: boolean("show_terminal_in_sidebar") + .notNull() + .default(true), + showFileManagerInSidebar: boolean("show_file_manager_in_sidebar") + .notNull() + .default(false), + showTunnelInSidebar: boolean("show_tunnel_in_sidebar") + .notNull() + .default(false), + showDockerInSidebar: boolean("show_docker_in_sidebar") + .notNull() + .default(false), + showServerStatsInSidebar: boolean("show_server_stats_in_sidebar") + .notNull() + .default(false), + defaultPath: text("default_path"), + statsConfig: text("stats_config"), + dockerConfig: text("docker_config"), + enableProxmox: boolean("enable_proxmox") + .notNull() + .default(false), + proxmoxConfig: text("proxmox_config"), + enableProxmoxStats: boolean("enable_proxmox_stats") + .notNull() + .default(false), + proxmoxStatsConfig: text("proxmox_stats_config"), + terminalConfig: text("terminal_config"), + quickActions: text("quick_actions"), + notes: text("notes"), + enableSsh: boolean("enable_ssh").notNull().default(true), + enableRdp: boolean("enable_rdp").notNull().default(false), + enableVnc: boolean("enable_vnc").notNull().default(false), + enableTelnet: boolean("enable_telnet").notNull().default(false), + + sshPort: int("ssh_port").default(22), + rdpPort: int("rdp_port").default(3389), + vncPort: int("vnc_port").default(5900), + telnetPort: int("telnet_port").default(23), + + rdpCredentialId: int("rdp_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + rdpUser: text("rdp_user"), + rdpPassword: text("rdp_password"), + rdpDomain: text("rdp_domain"), + rdpSecurity: text("rdp_security"), + rdpIgnoreCert: boolean("rdp_ignore_cert").default(false), + + vncCredentialId: int("vnc_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + vncPassword: text("vnc_password"), + vncUser: text("vnc_user"), + + telnetUser: text("telnet_user"), + telnetPassword: text("telnet_password"), + telnetCredentialId: int("telnet_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + + rdpAuthType: text("rdp_auth_type"), + vncAuthType: text("vnc_auth_type"), + telnetAuthType: text("telnet_auth_type"), + + domain: text("domain"), + security: text("security"), + ignoreCert: boolean("ignore_cert").default(false), + guacamoleConfig: text("guacamole_config"), + + useSocks5: boolean("use_socks5"), + socks5Host: text("socks5_host"), + socks5Port: int("socks5_port"), + socks5Username: text("socks5_username"), + socks5Password: text("socks5_password"), + socks5ProxyChain: text("socks5_proxy_chain"), + + // null = use the desktop app's global default; "local" | "remote" pins + // this specific host's SSH/Docker-console/Serial connections to originate + // from the embedded local backend or a connected remote sync server. + // Ignored for rdp/vnc/telnet, which always require the remote server. + connectionOrigin: text("connection_origin"), + + macAddress: text("mac_address"), + wolBroadcastAddress: text("wol_broadcast_address"), + portKnockSequence: text("port_knock_sequence"), + + hostKeyFingerprint: text("host_key_fingerprint"), + hostKeyType: text("host_key_type"), + hostKeyAlgorithm: text("host_key_algorithm").default("sha256"), + hostKeyFirstSeen: text("host_key_first_seen"), + hostKeyLastVerified: text("host_key_last_verified"), + hostKeyChangedCount: int("host_key_changed_count").default(0), + + // Stable identity used to match this row across two independently-seeded + // databases (the embedded backend and a connected remote server) during + // sync -- local autoincrement ids collide across instances. + syncId: varchar("sync_id", { length: 255 }).unique(), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Every host read is scoped by owner, so user_id carries the host list. + // + // `folder` is deliberately not indexed: on Postgres/MySQL an indexed text + // column is generated as varchar(255), and folder holds a joined nested path + // with no length cap, so indexing it would truncate deep hierarchies. + (table) => [ + index("idx_ssh_data_user_id").on(table.userId), + index("idx_ssh_data_parent_host").on(table.parentHostId), + index("idx_ssh_data_credential").on(table.credentialId), + ], +); + +export const fileManagerRecent = mysqlTable( + "file_manager_recent", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + path: text("path").notNull(), + lastOpened: text("last_opened") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Every file manager surface is read for one user on one host at a time. + (table) => [ + index("idx_file_manager_recent_user").on(table.userId, table.hostId), + ], +); + +export const fileManagerPinned = mysqlTable( + "file_manager_pinned", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + path: text("path").notNull(), + pinnedAt: text("pinned_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + index("idx_file_manager_pinned_user").on(table.userId, table.hostId), + ], +); + +export const fileManagerShortcuts = mysqlTable( + "file_manager_shortcuts", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + path: text("path").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + index("idx_file_manager_shortcuts_user").on(table.userId, table.hostId), + ], +); + +export const transferRecent = mysqlTable( + "transfer_recent", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + sourceHostId: int("source_host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + destHostId: int("dest_host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + destPath: text("dest_path").notNull(), + destPathLabel: text("dest_path_label").notNull(), + lastUsed: text("last_used") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [index("idx_transfer_recent_user").on(table.userId)], +); + +export const dismissedAlerts = mysqlTable( + "dismissed_alerts", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + alertId: text("alert_id").notNull(), + dismissedAt: text("dismissed_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [index("idx_dismissed_alerts_user_id").on(table.userId)], +); + +export const sshCredentials = mysqlTable( + "ssh_credentials", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + folder: text("folder"), + tags: text("tags"), + pin: boolean("pin").notNull().default(false), + // Manual drag-to-reorder position within a folder. Null means the + // credential has never been manually reordered; falls back to name sort + // in that case, same convention as hosts.sortOrder. + sortOrder: int("sort_order"), + authType: text("auth_type").notNull(), + username: text("username"), + password: text("password"), + key: text("key"), + privateKey: text("private_key"), + publicKey: text("public_key"), + keyPassword: text("key_password"), + keyType: text("key_type"), + detectedKeyType: text("detected_key_type"), + + certPublicKey: text("cert_public_key"), + + + usageCount: int("usage_count").notNull().default(0), + lastUsed: text("last_used"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [index("idx_ssh_credentials_user_id").on(table.userId)], +); + +export const sshCredentialUsage = mysqlTable( + "ssh_credential_usage", + { + id: int("id").autoincrement().primaryKey(), + credentialId: int("credential_id") + .notNull() + .references(() => sshCredentials.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + usedAt: text("used_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + index("idx_ssh_credential_usage_credential").on(table.credentialId), + index("idx_ssh_credential_usage_user").on(table.userId), + ], +); + +export const snippets = mysqlTable( + "snippets", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + content: text("content").notNull(), + description: text("description"), + folder: text("folder"), + order: int("order").notNull().default(0), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + hostFilter: text("host_filter"), + isNote: boolean("is_note").notNull().default(false), + }, + (table) => [index("idx_snippets_user_id").on(table.userId)], +); + +export const snippetFolders = mysqlTable("snippet_folders", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + color: text("color"), + icon: text("icon"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const c2sTunnelPresets = mysqlTable("c2s_tunnel_presets", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + config: text("config").notNull(), + platform: text("platform"), + computerName: text("computer_name"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const snippetAccess = mysqlTable( + "snippet_access", + { + id: int("id").autoincrement().primaryKey(), + snippetId: int("snippet_id") + .notNull() + .references(() => snippets.id, { onDelete: "cascade" }), + + userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "cascade" }), + roleId: int("role_id").references(() => roles.id, { + onDelete: "cascade", + }), + + grantedBy: varchar("granted_by", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + permissionLevel: text("permission_level").notNull().default("view"), + + expiresAt: varchar("expires_at", { length: 255 }), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Same three lookup shapes as host_access: by grantee, by role, by snippet. + (table) => [ + index("idx_snippet_access_user_id").on(table.userId), + index("idx_snippet_access_snippet_id").on(table.snippetId), + index("idx_snippet_access_role_id").on(table.roleId), + ], +); + +export const sshFolders = mysqlTable( + "ssh_folders", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + color: text("color"), + icon: text("icon"), + credentialId: int("credential_id").references(() => sshCredentials.id, { + onDelete: "set null", + }), + // Manual drag-to-reorder position among sibling folders. Null falls back + // to name sort, same convention as hosts.sortOrder. + sortOrder: int("sort_order"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [index("idx_ssh_folders_user_id").on(table.userId)], +); + +export const recentActivity = mysqlTable( + "recent_activity", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").notNull(), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + hostName: text("host_name"), + timestamp: varchar("timestamp", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Always read newest-first for one user, so timestamp follows user_id. + (table) => [ + index("idx_recent_activity_user_ts").on(table.userId, table.timestamp), + ], +); + +export const commandHistory = mysqlTable( + "command_history", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + command: text("command").notNull(), + executedAt: text("executed_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + index("idx_command_history_user_host").on(table.userId, table.hostId), + ], +); + +export const networkTopology = mysqlTable("network_topology", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + topology: text("topology"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const hostAccess = mysqlTable( + "host_access", + { + id: int("id").autoincrement().primaryKey(), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + + userId: varchar("user_id", { length: 255 }) + .references(() => users.id, { onDelete: "cascade" }), + roleId: int("role_id") + .references(() => roles.id, { onDelete: "cascade" }), + + grantedBy: varchar("granted_by", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + permissionLevel: text("permission_level") + .notNull() + .default("connect"), + + expiresAt: varchar("expires_at", { length: 255 }), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + lastAccessedAt: text("last_accessed_at"), + accessCount: int("access_count").notNull().default(0), + }, + // Resolved on every host list request and every permission check, so all + // three lookup shapes (by grantee, by role, by host) need to be indexed. + (table) => [ + index("idx_host_access_user_id").on(table.userId), + index("idx_host_access_role_id").on(table.roleId), + index("idx_host_access_host_id").on(table.hostId), + index("idx_host_access_expires_at").on(table.expiresAt), + ], +); + +export const sharedHostAuthOverrides = mysqlTable( + "shared_host_auth_overrides", + { + id: int("id").autoincrement().primaryKey(), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + protocol: varchar("protocol", { length: 255 }).notNull().default("ssh"), + credentialId: int("credential_id") + .notNull() + .references(() => sshCredentials.id, { onDelete: "cascade" }), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + uniqueIndex("shared_host_auth_overrides_host_user_protocol_unique").on( + table.hostId, + table.userId, + table.protocol, + ), + ], +); + +export const sharedHostSecrets = mysqlTable( + "shared_host_secrets", + { + id: int("id").autoincrement().primaryKey(), + + hostAccessId: int("host_access_id") + .notNull() + .references(() => hostAccess.id, { onDelete: "cascade" }), + + targetUserId: varchar("target_user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + protocol: varchar("protocol", { length: 255 }).notNull().default("ssh"), + sourceType: text("source_type").notNull().default("credential"), + + originalCredentialId: int("original_credential_id").references( + () => sshCredentials.id, + { onDelete: "cascade" }, + ), + + encryptedUsername: text("encrypted_username"), + encryptedAuthType: text("encrypted_auth_type"), + encryptedPassword: text("encrypted_password"), + encryptedKey: text("encrypted_key"), + encryptedKeyPassword: text("encrypted_key_password"), + encryptedKeyType: text("encrypted_key_type"), + encryptedDomain: text("encrypted_domain"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [ + uniqueIndex("idx_shared_host_secrets_scope").on( + table.hostAccessId, + table.targetUserId, + table.protocol, + ), + ], +); + +export const roles = mysqlTable("roles", { + id: int("id").autoincrement().primaryKey(), + name: varchar("name", { length: 255 }).notNull().unique(), + displayName: text("display_name").notNull(), + description: text("description"), + + isSystem: boolean("is_system") + .notNull() + .default(false), + + permissions: text("permissions"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const userRoles = mysqlTable( + "user_roles", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + roleId: int("role_id") + .notNull() + .references(() => roles.id, { onDelete: "cascade" }), + + grantedBy: varchar("granted_by", { length: 255 }).references(() => users.id, { + onDelete: "set null", + }), + grantedAt: text("granted_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + // + // The unique pair already serves lookups by user, since user_id leads it. + // Listing a role's members starts from role_id, which it cannot serve. + (table) => [ + uniqueIndex("idx_user_roles_user_role").on(table.userId, table.roleId), + index("idx_user_roles_role_id").on(table.roleId), + ], +); + +export const auditLogs = mysqlTable( + "audit_logs", + { + id: int("id").autoincrement().primaryKey(), + + // Nullable on purpose: the trail outlives the account, and username keeps the + // entry attributable once the reference is gone. + userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "set null" }), + username: text("username").notNull(), + + action: varchar("action", { length: 255 }).notNull(), + resourceType: varchar("resource_type", { length: 255 }).notNull(), + resourceId: text("resource_id"), + resourceName: text("resource_name"), + + details: text("details"), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + + success: boolean("success").notNull(), + errorMessage: text("error_message"), + + timestamp: varchar("timestamp", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // This table only grows, and is always read newest-first with an optional + // filter. Each composite leads with the filtered column so the same index + // also satisfies the ORDER BY. + (table) => [ + index("idx_audit_logs_timestamp").on(table.timestamp), + index("idx_audit_logs_user_ts").on(table.userId, table.timestamp), + index("idx_audit_logs_action_ts").on(table.action, table.timestamp), + index("idx_audit_logs_resource_ts").on(table.resourceType, table.timestamp), + ], +); + +export const sessionRecordings = mysqlTable( + "session_recordings", + { + id: int("id").autoincrement().primaryKey(), + + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + // Nullable on purpose: a recording is evidence about the host as much as the + // person, so it outlives the account. username keeps it attributable. + userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "set null" }), + username: text("username"), + accessId: int("access_id").references(() => hostAccess.id, { + onDelete: "set null", + }), + + startedAt: varchar("started_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + endedAt: text("ended_at"), + duration: int("duration"), + + commands: text("commands"), + dangerousActions: text("dangerous_actions"), + + recordingPath: text("recording_path"), + protocol: varchar("protocol", { length: 255 }).notNull().default("ssh"), + format: text("format").notNull().default("text"), + + terminatedByOwner: boolean("terminated_by_owner").default(false), + terminationReason: text("termination_reason"), + }, + // Listed newest-first per user, and audited per host. + (table) => [ + index("idx_session_recordings_user_started").on( + table.userId, + table.startedAt, + ), + index("idx_session_recordings_host").on(table.hostId), + ], +); + +export const sessionShares = mysqlTable( + "session_shares", + { + id: varchar("id", { length: 255 }).primaryKey(), + + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + ownerUserId: varchar("owner_user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + protocol: varchar("protocol", { length: 255 }).notNull(), + + // Live-session binding: TerminalSessionManager's session.id for SSH, or + // guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB + // row (process-local, in-memory) so this intentionally has no FK. + sessionId: varchar("session_id", { length: 255 }).notNull(), + tabInstanceId: text("tab_instance_id"), + + shareType: text("share_type").notNull(), // "link" | "user" + targetUserId: varchar("target_user_id", { length: 255 }).references(() => users.id, { + onDelete: "cascade", + }), + linkToken: varchar("link_token", { length: 255 }).unique(), + + permissionLevel: text("permission_level").notNull().default("read-only"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + revokedAt: text("revoked_at"), + + lastJoinedAt: text("last_joined_at"), + joinCount: int("join_count").notNull().default(0), + }, + // Resolved from the live session on join, and listed per host. + (table) => [ + index("idx_session_shares_session_id").on(table.sessionId), + index("idx_session_shares_host_id").on(table.hostId), + ], +); + +export const sessionShareParticipants = mysqlTable( + "session_share_participants", + { + id: int("id").autoincrement().primaryKey(), + shareId: varchar("share_id", { length: 255 }) + .notNull() + .references(() => sessionShares.id, { onDelete: "cascade" }), + + userId: varchar("user_id", { length: 255 }).references(() => users.id, { + onDelete: "cascade", + }), + guestLabel: text("guest_label"), + + joinedAt: text("joined_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + leftAt: text("left_at"), + }, +); + +export const opksshTokens = mysqlTable( + "opkssh_tokens", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + + sshCert: text("ssh_cert").notNull(), + privateKey: text("private_key").notNull(), + + email: text("email"), + sub: text("sub"), + issuer: text("issuer"), + audience: text("audience"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastUsed: text("last_used"), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [uniqueIndex("idx_opkssh_tokens_user_host").on(table.userId, table.hostId)], +); + +// Vault SSH signer profiles. These hold ONLY non-secret connection settings and +// are intended to be shared across users (shared === true makes a profile +// visible to every user on the server). Each user authenticates to Vault via an +// interactive OIDC flow at connect time; no tokens or keys are stored here. +export const vaultProfiles = mysqlTable("vault_profiles", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + folder: text("folder"), + tags: text("tags"), + // Vault server connection (non-secret) + vaultAddr: text("vault_addr").notNull(), + vaultNamespace: text("vault_namespace"), + // OIDC auth method mount + role used to obtain a Vault token interactively + oidcMount: text("oidc_mount"), + oidcRole: text("oidc_role"), + // SSH secrets engine mount + signer role used to sign the ephemeral key + sshMount: text("ssh_mount"), + sshRole: text("ssh_role").notNull(), + validPrincipals: text("valid_principals"), + // Ephemeral keypair algorithm to generate per connection + keyType: text("key_type"), + // When true the profile is visible/usable by all users on the server + shared: boolean("shared").notNull().default(false), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +// Per-user cache of the ephemeral SSH private key + Vault-signed certificate. +// Transient: rows live only until the certificate expires. Secret fields are +// encrypted under the user's data-encryption key (see field-crypto.ts). +export const vaultTokens = mysqlTable( + "vault_tokens", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + profileId: int("profile_id") + .notNull() + .references(() => vaultProfiles.id, { onDelete: "cascade" }), + + sshCert: text("ssh_cert").notNull(), + privateKey: text("private_key").notNull(), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastUsed: text("last_used"), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [uniqueIndex("idx_vault_tokens_user_profile").on(table.userId, table.profileId)], +); + +export const apiKeys = mysqlTable( + "api_keys", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + tokenHash: text("token_hash").notNull(), + tokenPrefix: text("token_prefix").notNull(), + createdAt: varchar("created_at", { length: 255 }).notNull().default(sql`(CURRENT_TIMESTAMP)`), + expiresAt: varchar("expires_at", { length: 255 }), + lastUsedAt: text("last_used_at"), + isActive: boolean("is_active").notNull().default(true), + }, + (table) => [index("idx_api_keys_user_id").on(table.userId)], +); + +export const userOpenTabs = mysqlTable( + "user_open_tabs", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tabType: text("tab_type").notNull(), + hostId: int("host_id").references(() => hosts.id, { + onDelete: "cascade", + }), + label: varchar("label", { length: 255 }).notNull(), + tabOrder: int("tab_order").notNull().default(0), + backendSessionId: text("backend_session_id"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [index("idx_user_open_tabs_user_id").on(table.userId)], +); + +export const userPreferences = mysqlTable("user_preferences", { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + reopenTabsOnLogin: boolean("reopen_tabs_on_login") + .notNull() + .default(false), + theme: text("theme"), + fontSize: text("font_size"), + accentColor: text("accent_color"), + language: text("language"), + storageMode: text("storage_mode"), + commandAutocomplete: boolean("command_autocomplete"), + commandPaletteEnabled: boolean("command_palette_enabled"), + showHostTags: boolean("show_host_tags"), + hostTrayOnClick: boolean("host_tray_on_click"), + pinAppRail: boolean("pin_app_rail"), + expandAppRailOnHover: boolean("expand_app_rail_on_hover"), + foldersCollapsed: boolean("folders_collapsed"), + confirmSnippetExecution: boolean("confirm_snippet_execution"), + disableUpdateCheck: boolean("disable_update_check"), + confirmTabClose: boolean("confirm_tab_close"), + hiddenRailTabs: text("hidden_rail_tabs"), + // null means the user has not been asked yet; the assistant stays hidden + // until this is explicitly true and the admin global is on. + aiAssistantEnabled: boolean("ai_assistant_enabled"), + // Opt-in to letting the assistant run allowlisted read-only diagnostics + // without a per-command approval click. + aiReadOnlyCommands: boolean("ai_read_only_commands"), + compactHostView: boolean("compact_host_view"), + statusColorScheme: text("status_color_scheme"), + customThemes: text("custom_themes"), + customKeybindings: text("custom_keybindings"), + terminalDefaults: text("terminal_defaults"), + rdpDefaults: text("rdp_defaults"), + terminalMacros: text("terminal_macros"), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const hostMetricsPreferences = mysqlTable( + "host_metrics_preferences", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + // JSON-encoded HostMetricsLayout. Layout has no secrets, so it is stored as + // plain JSON (no field-level encryption). + layout: text("layout").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // One layout per user per host. Enforced in production since the inline DDL + // creates it, but it was never declared here, so the generated Postgres and + // MySQL schemas lacked it โ€” and the upsert has nothing to conflict on. + (table) => [ + uniqueIndex("idx_host_metrics_prefs_user_host").on(table.userId, table.hostId), + ], +); + +export const proxmoxStatsPreferences = mysqlTable( + "proxmox_stats_preferences", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }).notNull().references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id").notNull().references(() => hosts.id, { onDelete: "cascade" }), + // JSON-encoded ProxmoxStatsLayout. Layout has no secrets, so it is stored as + // plain JSON (no field-level encryption), same convention as hostMetricsPreferences.layout. + layout: text("layout").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + uniqueIndex("idx_proxmox_stats_prefs_user_host").on(table.userId, table.hostId), + ], +); + +export const hostSidebarPreferences = mysqlTable("host_sidebar_preferences", { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded HostSidebarPreferences. No secrets in this blob, stored as + // plain JSON like hostMetricsPreferences.layout. + data: text("data").notNull(), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const credentialSidebarPreferences = mysqlTable( + "credential_sidebar_preferences", + { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded CredentialSidebarPreferences. No secrets in this blob, + // same convention as hostSidebarPreferences.data. + data: text("data").notNull(), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, +); + +export const uiPreferences = mysqlTable("ui_preferences", { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded UiPreferences (preset + per-area overrides + onboarding + // state). No secrets in this blob, same convention as + // hostSidebarPreferences.data. + data: text("data").notNull(), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const hostHealthChecks = mysqlTable( + "host_health_checks", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + // JSON array of { id, name, type: "tcp"|"http", target, port, path } + checks: text("checks").notNull(), + intervalSeconds: int("interval_seconds").notNull().default(300), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // Same as above: one set of checks per user per host. + (table) => [ + uniqueIndex("idx_host_health_checks_user_host").on(table.userId, table.hostId), + ], +); + +export const hostHealthHistory = mysqlTable("host_health_history", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + checkId: text("check_id").notNull(), + ts: text("ts").notNull().default(sql`(CURRENT_TIMESTAMP)`), + ok: boolean("ok").notNull(), + latencyMs: int("latency_ms"), + detail: text("detail"), +}); + +export const dashboardServiceLinks = mysqlTable("dashboard_service_links", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + label: varchar("label", { length: 255 }).notNull(), + url: text("url").notNull(), + order: int("order").notNull().default(0), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +// --- termix-id begin --- +// A user claims a unique public handle. Their published SSH public keys are +// served at an unauthenticated resolver endpoint in authorized_keys format, +// so any server can be provisioned with `curl /termix-id/u/ >> ~/.ssh/authorized_keys`. +export const termixIdentities = mysqlTable("termix_identities", { + id: int("id").autoincrement().primaryKey(), + // One Termix ID per user โ€” enforced in schema, not just in code. + userId: varchar("user_id", { length: 255 }) + .notNull() + .unique() + .references(() => users.id, { onDelete: "cascade" }), + handle: varchar("handle", { length: 255 }).notNull().unique(), + description: text("description"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const termixIdentityKeys = mysqlTable("termix_identity_keys", { + id: int("id").autoincrement().primaryKey(), + identityId: int("identity_id") + .notNull() + .references(() => termixIdentities.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // Public keys are non-secret, so they are stored in plaintext (no field-level + // encryption). This is what lets the unauthenticated resolver serve them. + publicKey: text("public_key").notNull(), + // Raw algorithm token (e.g. "ssh-ed25519"), and a normalized group used for + // the / resolver filter (RSA / ED25519 / ECDSA / ...). + keyType: text("key_type").notNull(), + algorithm: text("algorithm").notNull(), + label: varchar("label", { length: 255 }), + comment: text("comment"), + // "manual" (pasted) or "credential" (imported from an ssh_credentials entry). + source: text("source").notNull().default("manual"), + credentialId: int("credential_id").references(() => sshCredentials.id, { + onDelete: "set null", + }), + enabled: boolean("enabled").notNull().default(true), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); +// Per-identity certificate authority. Servers that trust this CA (via +// TrustedUserCAKeys / @cert-authority) accept any user certificate it signs, +// giving central revocation (rotate the CA) and expiry (cert validity). +export const termixIdentityCa = mysqlTable("termix_identity_ca", { + id: int("id").autoincrement().primaryKey(), + identityId: int("identity_id") + .notNull() + .unique() + .references(() => termixIdentities.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // CA public key (plaintext โ€” it is published); CA private key is field-encrypted. + publicKey: text("public_key").notNull(), + privateKey: text("private_key").notNull(), + validityDays: int("validity_days").notNull().default(90), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); +// --- termix-id end --- + +// --- tmux-monitor begin --- +export const tmuxSessionTags = mysqlTable("tmux_session_tags", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + sessionName: text("session_name").notNull(), + tag: text("tag").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); +// --- tmux-monitor end --- + +// --- metrics-history begin --- +export const hostMetricsHistory = mysqlTable("host_metrics_history", { + id: int("id").autoincrement().primaryKey(), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + ts: text("ts") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + cpuPercent: double("cpu_percent"), + memPercent: double("mem_percent"), + diskPercent: double("disk_percent"), + netRxBytes: int("net_rx_bytes"), + netTxBytes: int("net_tx_bytes"), +}); +// --- metrics-history end --- + +// --- proxmox-node-history begin --- +export const proxmoxNodeHistory = mysqlTable("proxmox_node_history", { + id: int("id").autoincrement().primaryKey(), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + ts: text("ts") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + cpuPercent: double("cpu_percent"), + memPercent: double("mem_percent"), + diskPercent: double("disk_percent"), + netRxBytes: int("net_rx_bytes"), + netTxBytes: int("net_tx_bytes"), +}); +// --- proxmox-node-history end --- + +// --- alerts begin --- +export const alertRules = mysqlTable("alert_rules", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: int("host_id").references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + enabled: boolean("enabled").notNull().default(true), + triggerType: text("trigger_type").notNull(), + thresholdValue: double("threshold_value"), + thresholdDurationSeconds: int("threshold_duration_seconds"), + cooldownMinutes: int("cooldown_minutes").notNull().default(15), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const notificationChannels = mysqlTable("notification_channels", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + type: text("type").notNull(), + config: text("config").notNull(), + enabled: boolean("enabled").notNull().default(true), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const alertRuleChannels = mysqlTable("alert_rule_channels", { + id: int("id").autoincrement().primaryKey(), + ruleId: int("rule_id") + .notNull() + .references(() => alertRules.id, { onDelete: "cascade" }), + channelId: int("channel_id") + .notNull() + .references(() => notificationChannels.id, { onDelete: "cascade" }), +}); + +export const alertFirings = mysqlTable( + "alert_firings", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + ruleId: int("rule_id") + .notNull() + .references(() => alertRules.id, { onDelete: "cascade" }), + hostId: int("host_id").notNull(), + hostName: text("host_name").notNull(), + firedAt: varchar("fired_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + resolvedAt: text("resolved_at"), + value: double("value"), + message: text("message").notNull(), + severity: text("severity").notNull().default("warning"), + acknowledged: boolean("acknowledged") + .notNull() + .default(false), + }, + // A rule's history is read newest-first; host_id is filtered on its own. + (table) => [ + index("idx_alert_firings_rule").on(table.ruleId, table.firedAt), + index("idx_alert_firings_host").on(table.hostId), + ], +); +// --- alerts end --- + +// --- automations begin --- +export const automations = mysqlTable( + "automations", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + enabled: boolean("enabled").notNull().default(true), + // The whole trigger + steps graph, shaped by AutomationDefinition. Read and + // written as a unit, never queried by its inner structure. + definition: text("definition").notNull(), + definitionVersion: int("definition_version").notNull().default(1), + concurrencyPolicy: text("concurrency_policy").notNull().default("skip"), + maxRunSeconds: int("max_run_seconds").notNull().default(300), + dryRun: boolean("dry_run").notNull().default(false), + lastRunAt: text("last_run_at"), + lastRunStatus: text("last_run_status"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // The scheduler sweeps enabled automations for one user at a time. + (table) => [index("idx_automations_user").on(table.userId, table.enabled)], +); + +/** + * Durable per-target trigger state. state_key scopes a trigger to what it is + * actually watching ("", ":/data", ":"), so + * a sustained-breach window can track one filesystem rather than a whole host. + * Living in the database rather than memory means cooldowns and dwell windows + * survive a restart. + */ +export const automationTriggerState = mysqlTable( + "automation_trigger_state", + { + id: int("id").autoincrement().primaryKey(), + automationId: int("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + stateKey: varchar("state_key", { length: 255 }).notNull(), + breachStartedAt: text("breach_started_at"), + lastFiredAt: text("last_fired_at"), + lastValue: double("last_value"), + lastObservedState: text("last_observed_state"), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + uniqueIndex("idx_automation_trigger_state_key").on( + table.automationId, + table.stateKey, + ), + ], +); + +export const automationSchedules = mysqlTable( + "automation_schedules", + { + id: int("id").autoincrement().primaryKey(), + automationId: int("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + cron: text("cron"), + intervalSeconds: int("interval_seconds"), + timezone: text("timezone"), + nextDueAt: varchar("next_due_at", { length: 255 }), + lastTickAt: text("last_tick_at"), + }, + (table) => [ + uniqueIndex("idx_automation_schedules_automation").on(table.automationId), + index("idx_automation_schedules_due").on(table.nextDueAt), + ], +); + +export const automationRuns = mysqlTable( + "automation_runs", + { + id: int("id").autoincrement().primaryKey(), + automationId: int("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + triggerType: text("trigger_type").notNull(), + triggerContext: text("trigger_context"), + status: varchar("status", { length: 255 }).notNull(), + startedAt: varchar("started_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + finishedAt: text("finished_at"), + durationMs: int("duration_ms"), + error: text("error"), + dryRun: boolean("dry_run").notNull().default(false), + // Set when one automation invoked another, so a chain can be traced. + parentRunId: int("parent_run_id"), + }, + (table) => [ + index("idx_automation_runs_automation").on( + table.automationId, + table.startedAt, + ), + index("idx_automation_runs_user").on(table.userId, table.startedAt), + ], +); + +export const automationRunSteps = mysqlTable( + "automation_run_steps", + { + id: int("id").autoincrement().primaryKey(), + runId: int("run_id") + .notNull() + .references(() => automationRuns.id, { onDelete: "cascade" }), + stepIndex: int("step_index").notNull(), + stepId: text("step_id").notNull(), + stepType: text("step_type").notNull(), + status: varchar("status", { length: 255 }).notNull(), + startedAt: varchar("started_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + finishedAt: text("finished_at"), + output: text("output"), + error: text("error"), + truncated: boolean("truncated") + .notNull() + .default(false), + }, + (table) => [ + index("idx_automation_run_steps_run").on(table.runId, table.stepIndex), + ], +); + +export const automationChannels = mysqlTable( + "automation_channels", + { + id: int("id").autoincrement().primaryKey(), + automationId: int("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + channelId: int("channel_id") + .notNull() + .references(() => notificationChannels.id, { onDelete: "cascade" }), + }, + (table) => [ + uniqueIndex("idx_automation_channels_pair").on( + table.automationId, + table.channelId, + ), + ], +); +// --- automations end --- + +// --- homepage begin --- +export const homepageItems = mysqlTable( + "homepage_items", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + typeId: text("type_id").notNull(), + title: text("title"), + config: text("config").notNull().default("{}"), + folderId: int("folder_id"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [index("idx_homepage_items_user_id").on(table.userId)], +); + +export const homepageLayouts = mysqlTable("homepage_layouts", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .unique() + .references(() => users.id, { onDelete: "cascade" }), + // JSON: { entries: HomepageLayoutEntry[], pan: {x,y}, zoom: number } + layout: text("layout").notNull().default("{}"), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); +// --- homepage end --- + +// --- fleets begin --- +export const fleets = mysqlTable("fleets", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + color: text("color"), + icon: text("icon"), + // JSON array of { tag: string } rules, unioned with static fleetMembers at + // resolution time. Kept to tag-equality matching for v1. + tagRules: text("tag_rules"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const fleetMembers = mysqlTable( + "fleet_members", + { + id: int("id").autoincrement().primaryKey(), + fleetId: int("fleet_id") + .notNull() + .references(() => fleets.id, { onDelete: "cascade" }), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + addedAt: text("added_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // fleet_id leads the unique pair, so listing a fleet's hosts is already + // served. Finding the fleets a host belongs to starts from host_id. + (table) => [ + uniqueIndex("idx_fleet_members_fleet_host").on(table.fleetId, table.hostId), + index("idx_fleet_members_host").on(table.hostId), + ], +); + +// Latest-only inventory snapshot per host, overwritten on each refresh - no +// historical log, matching the "latest snapshot only" scope decision. +export const fleetInventory = mysqlTable( + "fleet_inventory", + { + id: int("id").autoincrement().primaryKey(), + hostId: int("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + osPrettyName: text("os_pretty_name"), + kernel: text("kernel"), + architecture: text("architecture"), + hostname: text("hostname"), + uptimeSeconds: int("uptime_seconds"), + ip: text("ip"), + packageManager: text("package_manager"), + collectedAt: text("collected_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + // host_id leads the unique pair; a user's whole inventory is read by user_id. + (table) => [ + uniqueIndex("idx_fleet_inventory_host").on(table.hostId, table.userId), + index("idx_fleet_inventory_user").on(table.userId), + ], +); +// --- fleets end --- + +// --- workspaces begin --- +export const userWorkspaces = mysqlTable( + "user_workspaces", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + color: text("color"), + icon: text("icon"), + // "manual" | "last_session" - exactly one last_session row per user. + kind: text("kind").notNull().default("manual"), + isDefault: boolean("is_default") + .notNull() + .default(false), + // JSON-encoded WorkspacePayload: tabs, splitMode, paneTabIds, rowSizes, rowColSizes + payload: text("payload").notNull().default("{}"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + lastUsedAt: text("last_used_at"), + }, + (table) => [index("idx_user_workspaces_user_id").on(table.userId)], +); +// --- workspaces end --- + +// --- sync begin --- +// Records a delete for a synced entity type so the other side of a sync +// pair (embedded desktop backend <-> connected remote server) learns about +// the deletion instead of re-creating the row on its next pull. +export const syncTombstones = mysqlTable("sync_tombstones", { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + entityType: text("entity_type").notNull(), + syncId: varchar("sync_id", { length: 255 }).notNull(), + deletedAt: text("deleted_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), +}); +// --- sync end --- + +// --- ai begin --- +/** + * A user's connection to one AI provider. api_key is encrypted at rest via + * FieldCrypto; it is never returned to the frontend, which only ever sees + * api_key_prefix for display. + */ +export const aiProviders = mysqlTable( + "ai_providers", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // ollama | anthropic | openai | gemini | openai_compatible + providerType: text("provider_type").notNull(), + label: varchar("label", { length: 255 }).notNull(), + // Required for ollama and openai_compatible, optional elsewhere. + baseUrl: text("base_url"), + apiKey: text("api_key"), + // First few characters, kept in the clear so the UI can identify a key. + apiKeyPrefix: text("api_key_prefix"), + defaultModel: text("default_model"), + enabled: boolean("enabled").notNull().default(true), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + uniqueIndex("idx_ai_providers_user_label").on(table.userId, table.label), + ], +); + +export const aiConversations = mysqlTable( + "ai_conversations", + { + id: int("id").autoincrement().primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + title: text("title"), + providerId: int("provider_id"), + model: text("model"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + index("idx_ai_conversations_user").on(table.userId, table.updatedAt), + ], +); + +export const aiMessages = mysqlTable( + "ai_messages", + { + id: int("id").autoincrement().primaryKey(), + conversationId: int("conversation_id") + .notNull() + .references(() => aiConversations.id, { onDelete: "cascade" }), + // user | assistant | tool + role: text("role").notNull(), + content: text("content").notNull().default(""), + // Serialized tool calls and their results for this turn. + toolCalls: text("tool_calls"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + index("idx_ai_messages_conversation").on( + table.conversationId, + table.createdAt, + ), + ], +); + +/** + * A change the assistant wants to make. Nothing here has been applied: the + * payload is re-validated against the tool schema at apply time and only then + * dispatched through the same repository logic a human action uses. + */ +export const aiProposals = mysqlTable( + "ai_proposals", + { + id: int("id").autoincrement().primaryKey(), + conversationId: int("conversation_id") + .notNull() + .references(() => aiConversations.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // The propose_* tool name that produced this. + kind: text("kind").notNull(), + summary: text("summary"), + payload: text("payload").notNull().default("{}"), + // pending | applied | rejected | expired + status: varchar("status", { length: 255 }).notNull().default("pending"), + appliedAt: text("applied_at"), + resultSummary: text("result_summary"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + }, + (table) => [ + index("idx_ai_proposals_user").on(table.userId, table.status), + index("idx_ai_proposals_conversation").on(table.conversationId), + ], +); +// --- ai end --- diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts new file mode 100644 index 0000000..c5d0327 --- /dev/null +++ b/src/backend/database/db/schema.pg.ts @@ -0,0 +1,1942 @@ +// GENERATED FILE โ€” do not edit. +// +// Produced from schema.ts by scripts/generate-dialect-schema.cjs. +// Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`. +// Target dialect: postgres. +// +// DDL source for drizzle-kit. NOT imported to run queries โ€” repositories use +// schema.ts on every dialect. See the generator header for why that is correct. + +import { + pgTable, + text, + varchar, + integer, + serial, + boolean, + doublePrecision, + index, + uniqueIndex, + type AnyPgColumn, +} from "drizzle-orm/pg-core"; +import { sql } from "drizzle-orm"; + +export const users = pgTable("users", { + id: varchar("id", { length: 255 }).primaryKey(), + username: text("username").notNull(), + passwordHash: text("password_hash").notNull(), + isAdmin: boolean("is_admin").notNull().default(false), + + isOidc: boolean("is_oidc").notNull().default(false), + oidcIdentifier: text("oidc_identifier"), + ssoProviderId: integer("sso_provider_id"), + clientId: text("client_id"), + clientSecret: text("client_secret"), + issuerUrl: text("issuer_url"), + authorizationUrl: text("authorization_url"), + tokenUrl: text("token_url"), + identifierPath: text("identifier_path"), + namePath: text("name_path"), + scopes: text().default("openid email profile"), + + totpSecret: text("totp_secret"), + totpEnabled: boolean("totp_enabled") + .notNull() + .default(false), + totpBackupCodes: text("totp_backup_codes"), + + registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`), + donationModalDismissed: boolean("donation_modal_dismissed") + .notNull() + .default(false), +}); + +export const settings = pgTable("settings", { + key: varchar("key", { length: 255 }).primaryKey(), + value: text("value").notNull(), +}); + +export const ssoProviders = pgTable("sso_providers", { + id: serial("id").primaryKey(), + name: varchar("name", { length: 255 }).notNull(), + type: text("type").notNull(), + enabled: boolean("enabled").notNull().default(true), + displayOrder: integer("display_order").notNull().default(0), + config: text("config").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const sessions = pgTable( + "sessions", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + jwtToken: text("jwt_token").notNull(), + deviceType: text("device_type").notNull(), + deviceInfo: text("device_info").notNull(), + oidcSub: text("oidc_sub"), + oidcSid: text("oidc_sid"), + ssoProviderId: integer("sso_provider_id"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastActiveAt: text("last_active_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Listing a user's devices, and the startup sweep of expired rows. + (table) => [ + index("idx_sessions_user_id").on(table.userId), + index("idx_sessions_expires_at").on(table.expiresAt), + ], +); + +export const trustedDevices = pgTable( + "trusted_devices", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + deviceFingerprint: text("device_fingerprint").notNull(), + deviceType: text("device_type").notNull(), + deviceInfo: text("device_info").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastUsedAt: text("last_used_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_trusted_devices_user_id").on(table.userId)], +); + +export const webauthnCredentials = pgTable("webauthn_credentials", { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + credentialId: varchar("credential_id", { length: 255 }).notNull(), + publicKey: text("public_key").notNull(), + counter: integer("counter").notNull().default(0), + deviceType: text("device_type"), + backedUp: boolean("backed_up").notNull().default(false), + transports: text("transports"), + userVerification: text("user_verification").notNull().default("preferred"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + lastUsedAt: text("last_used_at"), +}); + +export const hosts = pgTable( + "ssh_data", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + connectionType: text("connection_type").notNull().default("ssh"), + name: varchar("name", { length: 255 }), + ip: text("ip").notNull(), + port: integer("port").notNull(), + username: text("username").notNull(), + folder: text("folder"), + // Sub-host nesting: a host acting as an organizational parent for other + // hosts, mutually exclusive with folder (see host route validation). + parentHostId: integer("parent_host_id").references( + (): AnyPgColumn => hosts.id, + { onDelete: "set null" }, + ), + tags: text("tags"), + pin: boolean("pin").notNull().default(false), + // Manual drag-to-reorder position within a folder. Null means the host has + // never been manually reordered; falls back to name sort in that case. + sortOrder: integer("sort_order"), + authType: text("auth_type").notNull(), + useWarpgate: boolean("use_warpgate").notNull().default(false), + shareSshAuth: boolean("share_ssh_auth") + .notNull() + .default(false), + forceKeyboardInteractive: text("force_keyboard_interactive"), + + password: text("password"), + key: text("key"), + keyPassword: text("key_password"), + keyType: text("key_type"), + sudoPassword: text("sudo_password"), + + autostartPassword: text("autostart_password"), + autostartKey: text("autostart_key"), + autostartKeyPassword: text("autostart_key_password"), + + credentialId: integer("credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + overrideCredentialUsername: boolean("override_credential_username"), + // When authType is "vault", the host authenticates via a Vault SSH signer + // profile (shared settings, no secrets). The signing certificate is obtained + // per-user at connect time via an interactive Vault OIDC flow. + vaultProfileId: integer("vault_profile_id").references( + () => vaultProfiles.id, + { onDelete: "set null" }, + ), + enableTerminal: boolean("enable_terminal") + .notNull() + .default(true), + enableSessionLogging: boolean("enable_session_logging") + .notNull() + .default(true), + allowSessionSharing: boolean("allow_session_sharing") + .notNull() + .default(true), + enableCommandHistory: boolean("enable_command_history") + .notNull() + .default(true), + enableTunnel: boolean("enable_tunnel") + .notNull() + .default(true), + tunnelConnections: text("tunnel_connections"), + jumpHosts: text("jump_hosts"), + enableFileManager: boolean("enable_file_manager") + .notNull() + .default(true), + scpLegacy: boolean("scp_legacy").notNull().default(false), + enableDocker: boolean("enable_docker") + .notNull() + .default(false), + enableTmuxMonitor: boolean("enable_tmux_monitor") + .notNull() + .default(false), + enableTerminalToolbar: boolean("enable_terminal_toolbar") + .notNull() + .default(true), + showTerminalInSidebar: boolean("show_terminal_in_sidebar") + .notNull() + .default(true), + showFileManagerInSidebar: boolean("show_file_manager_in_sidebar") + .notNull() + .default(false), + showTunnelInSidebar: boolean("show_tunnel_in_sidebar") + .notNull() + .default(false), + showDockerInSidebar: boolean("show_docker_in_sidebar") + .notNull() + .default(false), + showServerStatsInSidebar: boolean("show_server_stats_in_sidebar") + .notNull() + .default(false), + defaultPath: text("default_path"), + statsConfig: text("stats_config"), + dockerConfig: text("docker_config"), + enableProxmox: boolean("enable_proxmox") + .notNull() + .default(false), + proxmoxConfig: text("proxmox_config"), + enableProxmoxStats: boolean("enable_proxmox_stats") + .notNull() + .default(false), + proxmoxStatsConfig: text("proxmox_stats_config"), + terminalConfig: text("terminal_config"), + quickActions: text("quick_actions"), + notes: text("notes"), + enableSsh: boolean("enable_ssh").notNull().default(true), + enableRdp: boolean("enable_rdp").notNull().default(false), + enableVnc: boolean("enable_vnc").notNull().default(false), + enableTelnet: boolean("enable_telnet").notNull().default(false), + + sshPort: integer("ssh_port").default(22), + rdpPort: integer("rdp_port").default(3389), + vncPort: integer("vnc_port").default(5900), + telnetPort: integer("telnet_port").default(23), + + rdpCredentialId: integer("rdp_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + rdpUser: text("rdp_user"), + rdpPassword: text("rdp_password"), + rdpDomain: text("rdp_domain"), + rdpSecurity: text("rdp_security"), + rdpIgnoreCert: boolean("rdp_ignore_cert").default(false), + + vncCredentialId: integer("vnc_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + vncPassword: text("vnc_password"), + vncUser: text("vnc_user"), + + telnetUser: text("telnet_user"), + telnetPassword: text("telnet_password"), + telnetCredentialId: integer("telnet_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + + rdpAuthType: text("rdp_auth_type"), + vncAuthType: text("vnc_auth_type"), + telnetAuthType: text("telnet_auth_type"), + + domain: text("domain"), + security: text("security"), + ignoreCert: boolean("ignore_cert").default(false), + guacamoleConfig: text("guacamole_config"), + + useSocks5: boolean("use_socks5"), + socks5Host: text("socks5_host"), + socks5Port: integer("socks5_port"), + socks5Username: text("socks5_username"), + socks5Password: text("socks5_password"), + socks5ProxyChain: text("socks5_proxy_chain"), + + // null = use the desktop app's global default; "local" | "remote" pins + // this specific host's SSH/Docker-console/Serial connections to originate + // from the embedded local backend or a connected remote sync server. + // Ignored for rdp/vnc/telnet, which always require the remote server. + connectionOrigin: text("connection_origin"), + + macAddress: text("mac_address"), + wolBroadcastAddress: text("wol_broadcast_address"), + portKnockSequence: text("port_knock_sequence"), + + hostKeyFingerprint: text("host_key_fingerprint"), + hostKeyType: text("host_key_type"), + hostKeyAlgorithm: text("host_key_algorithm").default("sha256"), + hostKeyFirstSeen: text("host_key_first_seen"), + hostKeyLastVerified: text("host_key_last_verified"), + hostKeyChangedCount: integer("host_key_changed_count").default(0), + + // Stable identity used to match this row across two independently-seeded + // databases (the embedded backend and a connected remote server) during + // sync -- local autoincrement ids collide across instances. + syncId: varchar("sync_id", { length: 255 }).unique(), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Every host read is scoped by owner, so user_id carries the host list. + // + // `folder` is deliberately not indexed: on Postgres/MySQL an indexed text + // column is generated as varchar(255), and folder holds a joined nested path + // with no length cap, so indexing it would truncate deep hierarchies. + (table) => [ + index("idx_ssh_data_user_id").on(table.userId), + index("idx_ssh_data_parent_host").on(table.parentHostId), + index("idx_ssh_data_credential").on(table.credentialId), + ], +); + +export const fileManagerRecent = pgTable( + "file_manager_recent", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + path: text("path").notNull(), + lastOpened: text("last_opened") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Every file manager surface is read for one user on one host at a time. + (table) => [ + index("idx_file_manager_recent_user").on(table.userId, table.hostId), + ], +); + +export const fileManagerPinned = pgTable( + "file_manager_pinned", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + path: text("path").notNull(), + pinnedAt: text("pinned_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_file_manager_pinned_user").on(table.userId, table.hostId), + ], +); + +export const fileManagerShortcuts = pgTable( + "file_manager_shortcuts", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + path: text("path").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_file_manager_shortcuts_user").on(table.userId, table.hostId), + ], +); + +export const transferRecent = pgTable( + "transfer_recent", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + sourceHostId: integer("source_host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + destHostId: integer("dest_host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + destPath: text("dest_path").notNull(), + destPathLabel: text("dest_path_label").notNull(), + lastUsed: text("last_used") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_transfer_recent_user").on(table.userId)], +); + +export const dismissedAlerts = pgTable( + "dismissed_alerts", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + alertId: text("alert_id").notNull(), + dismissedAt: text("dismissed_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_dismissed_alerts_user_id").on(table.userId)], +); + +export const sshCredentials = pgTable( + "ssh_credentials", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + folder: text("folder"), + tags: text("tags"), + pin: boolean("pin").notNull().default(false), + // Manual drag-to-reorder position within a folder. Null means the + // credential has never been manually reordered; falls back to name sort + // in that case, same convention as hosts.sortOrder. + sortOrder: integer("sort_order"), + authType: text("auth_type").notNull(), + username: text("username"), + password: text("password"), + key: text("key"), + privateKey: text("private_key"), + publicKey: text("public_key"), + keyPassword: text("key_password"), + keyType: text("key_type"), + detectedKeyType: text("detected_key_type"), + + certPublicKey: text("cert_public_key"), + + + usageCount: integer("usage_count").notNull().default(0), + lastUsed: text("last_used"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_ssh_credentials_user_id").on(table.userId)], +); + +export const sshCredentialUsage = pgTable( + "ssh_credential_usage", + { + id: serial("id").primaryKey(), + credentialId: integer("credential_id") + .notNull() + .references(() => sshCredentials.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + usedAt: text("used_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ssh_credential_usage_credential").on(table.credentialId), + index("idx_ssh_credential_usage_user").on(table.userId), + ], +); + +export const snippets = pgTable( + "snippets", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + content: text("content").notNull(), + description: text("description"), + folder: text("folder"), + order: integer("order").notNull().default(0), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + hostFilter: text("host_filter"), + isNote: boolean("is_note").notNull().default(false), + }, + (table) => [index("idx_snippets_user_id").on(table.userId)], +); + +export const snippetFolders = pgTable("snippet_folders", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + color: text("color"), + icon: text("icon"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const c2sTunnelPresets = pgTable("c2s_tunnel_presets", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + config: text("config").notNull(), + platform: text("platform"), + computerName: text("computer_name"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const snippetAccess = pgTable( + "snippet_access", + { + id: serial("id").primaryKey(), + snippetId: integer("snippet_id") + .notNull() + .references(() => snippets.id, { onDelete: "cascade" }), + + userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id").references(() => roles.id, { + onDelete: "cascade", + }), + + grantedBy: varchar("granted_by", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + permissionLevel: text("permission_level").notNull().default("view"), + + expiresAt: varchar("expires_at", { length: 255 }), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Same three lookup shapes as host_access: by grantee, by role, by snippet. + (table) => [ + index("idx_snippet_access_user_id").on(table.userId), + index("idx_snippet_access_snippet_id").on(table.snippetId), + index("idx_snippet_access_role_id").on(table.roleId), + ], +); + +export const sshFolders = pgTable( + "ssh_folders", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + color: text("color"), + icon: text("icon"), + credentialId: integer("credential_id").references(() => sshCredentials.id, { + onDelete: "set null", + }), + // Manual drag-to-reorder position among sibling folders. Null falls back + // to name sort, same convention as hosts.sortOrder. + sortOrder: integer("sort_order"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_ssh_folders_user_id").on(table.userId)], +); + +export const recentActivity = pgTable( + "recent_activity", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").notNull(), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + hostName: text("host_name"), + timestamp: varchar("timestamp", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Always read newest-first for one user, so timestamp follows user_id. + (table) => [ + index("idx_recent_activity_user_ts").on(table.userId, table.timestamp), + ], +); + +export const commandHistory = pgTable( + "command_history", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + command: text("command").notNull(), + executedAt: text("executed_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_command_history_user_host").on(table.userId, table.hostId), + ], +); + +export const networkTopology = pgTable("network_topology", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + topology: text("topology"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const hostAccess = pgTable( + "host_access", + { + id: serial("id").primaryKey(), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + + userId: varchar("user_id", { length: 255 }) + .references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id") + .references(() => roles.id, { onDelete: "cascade" }), + + grantedBy: varchar("granted_by", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + permissionLevel: text("permission_level") + .notNull() + .default("connect"), + + expiresAt: varchar("expires_at", { length: 255 }), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + lastAccessedAt: text("last_accessed_at"), + accessCount: integer("access_count").notNull().default(0), + }, + // Resolved on every host list request and every permission check, so all + // three lookup shapes (by grantee, by role, by host) need to be indexed. + (table) => [ + index("idx_host_access_user_id").on(table.userId), + index("idx_host_access_role_id").on(table.roleId), + index("idx_host_access_host_id").on(table.hostId), + index("idx_host_access_expires_at").on(table.expiresAt), + ], +); + +export const sharedHostAuthOverrides = pgTable( + "shared_host_auth_overrides", + { + id: serial("id").primaryKey(), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + protocol: varchar("protocol", { length: 255 }).notNull().default("ssh"), + credentialId: integer("credential_id") + .notNull() + .references(() => sshCredentials.id, { onDelete: "cascade" }), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("shared_host_auth_overrides_host_user_protocol_unique").on( + table.hostId, + table.userId, + table.protocol, + ), + ], +); + +export const sharedHostSecrets = pgTable( + "shared_host_secrets", + { + id: serial("id").primaryKey(), + + hostAccessId: integer("host_access_id") + .notNull() + .references(() => hostAccess.id, { onDelete: "cascade" }), + + targetUserId: varchar("target_user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + protocol: varchar("protocol", { length: 255 }).notNull().default("ssh"), + sourceType: text("source_type").notNull().default("credential"), + + originalCredentialId: integer("original_credential_id").references( + () => sshCredentials.id, + { onDelete: "cascade" }, + ), + + encryptedUsername: text("encrypted_username"), + encryptedAuthType: text("encrypted_auth_type"), + encryptedPassword: text("encrypted_password"), + encryptedKey: text("encrypted_key"), + encryptedKeyPassword: text("encrypted_key_password"), + encryptedKeyType: text("encrypted_key_type"), + encryptedDomain: text("encrypted_domain"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [ + uniqueIndex("idx_shared_host_secrets_scope").on( + table.hostAccessId, + table.targetUserId, + table.protocol, + ), + ], +); + +export const roles = pgTable("roles", { + id: serial("id").primaryKey(), + name: varchar("name", { length: 255 }).notNull().unique(), + displayName: text("display_name").notNull(), + description: text("description"), + + isSystem: boolean("is_system") + .notNull() + .default(false), + + permissions: text("permissions"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const userRoles = pgTable( + "user_roles", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id") + .notNull() + .references(() => roles.id, { onDelete: "cascade" }), + + grantedBy: varchar("granted_by", { length: 255 }).references(() => users.id, { + onDelete: "set null", + }), + grantedAt: text("granted_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + // + // The unique pair already serves lookups by user, since user_id leads it. + // Listing a role's members starts from role_id, which it cannot serve. + (table) => [ + uniqueIndex("idx_user_roles_user_role").on(table.userId, table.roleId), + index("idx_user_roles_role_id").on(table.roleId), + ], +); + +export const auditLogs = pgTable( + "audit_logs", + { + id: serial("id").primaryKey(), + + // Nullable on purpose: the trail outlives the account, and username keeps the + // entry attributable once the reference is gone. + userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "set null" }), + username: text("username").notNull(), + + action: varchar("action", { length: 255 }).notNull(), + resourceType: varchar("resource_type", { length: 255 }).notNull(), + resourceId: text("resource_id"), + resourceName: text("resource_name"), + + details: text("details"), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + + success: boolean("success").notNull(), + errorMessage: text("error_message"), + + timestamp: varchar("timestamp", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // This table only grows, and is always read newest-first with an optional + // filter. Each composite leads with the filtered column so the same index + // also satisfies the ORDER BY. + (table) => [ + index("idx_audit_logs_timestamp").on(table.timestamp), + index("idx_audit_logs_user_ts").on(table.userId, table.timestamp), + index("idx_audit_logs_action_ts").on(table.action, table.timestamp), + index("idx_audit_logs_resource_ts").on(table.resourceType, table.timestamp), + ], +); + +export const sessionRecordings = pgTable( + "session_recordings", + { + id: serial("id").primaryKey(), + + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + // Nullable on purpose: a recording is evidence about the host as much as the + // person, so it outlives the account. username keeps it attributable. + userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "set null" }), + username: text("username"), + accessId: integer("access_id").references(() => hostAccess.id, { + onDelete: "set null", + }), + + startedAt: varchar("started_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + endedAt: text("ended_at"), + duration: integer("duration"), + + commands: text("commands"), + dangerousActions: text("dangerous_actions"), + + recordingPath: text("recording_path"), + protocol: varchar("protocol", { length: 255 }).notNull().default("ssh"), + format: text("format").notNull().default("text"), + + terminatedByOwner: boolean("terminated_by_owner").default(false), + terminationReason: text("termination_reason"), + }, + // Listed newest-first per user, and audited per host. + (table) => [ + index("idx_session_recordings_user_started").on( + table.userId, + table.startedAt, + ), + index("idx_session_recordings_host").on(table.hostId), + ], +); + +export const sessionShares = pgTable( + "session_shares", + { + id: varchar("id", { length: 255 }).primaryKey(), + + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + ownerUserId: varchar("owner_user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + protocol: varchar("protocol", { length: 255 }).notNull(), + + // Live-session binding: TerminalSessionManager's session.id for SSH, or + // guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB + // row (process-local, in-memory) so this intentionally has no FK. + sessionId: varchar("session_id", { length: 255 }).notNull(), + tabInstanceId: text("tab_instance_id"), + + shareType: text("share_type").notNull(), // "link" | "user" + targetUserId: varchar("target_user_id", { length: 255 }).references(() => users.id, { + onDelete: "cascade", + }), + linkToken: varchar("link_token", { length: 255 }).unique(), + + permissionLevel: text("permission_level").notNull().default("read-only"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + revokedAt: text("revoked_at"), + + lastJoinedAt: text("last_joined_at"), + joinCount: integer("join_count").notNull().default(0), + }, + // Resolved from the live session on join, and listed per host. + (table) => [ + index("idx_session_shares_session_id").on(table.sessionId), + index("idx_session_shares_host_id").on(table.hostId), + ], +); + +export const sessionShareParticipants = pgTable( + "session_share_participants", + { + id: serial("id").primaryKey(), + shareId: varchar("share_id", { length: 255 }) + .notNull() + .references(() => sessionShares.id, { onDelete: "cascade" }), + + userId: varchar("user_id", { length: 255 }).references(() => users.id, { + onDelete: "cascade", + }), + guestLabel: text("guest_label"), + + joinedAt: text("joined_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + leftAt: text("left_at"), + }, +); + +export const opksshTokens = pgTable( + "opkssh_tokens", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + + sshCert: text("ssh_cert").notNull(), + privateKey: text("private_key").notNull(), + + email: text("email"), + sub: text("sub"), + issuer: text("issuer"), + audience: text("audience"), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastUsed: text("last_used"), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [uniqueIndex("idx_opkssh_tokens_user_host").on(table.userId, table.hostId)], +); + +// Vault SSH signer profiles. These hold ONLY non-secret connection settings and +// are intended to be shared across users (shared === true makes a profile +// visible to every user on the server). Each user authenticates to Vault via an +// interactive OIDC flow at connect time; no tokens or keys are stored here. +export const vaultProfiles = pgTable("vault_profiles", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + folder: text("folder"), + tags: text("tags"), + // Vault server connection (non-secret) + vaultAddr: text("vault_addr").notNull(), + vaultNamespace: text("vault_namespace"), + // OIDC auth method mount + role used to obtain a Vault token interactively + oidcMount: text("oidc_mount"), + oidcRole: text("oidc_role"), + // SSH secrets engine mount + signer role used to sign the ephemeral key + sshMount: text("ssh_mount"), + sshRole: text("ssh_role").notNull(), + validPrincipals: text("valid_principals"), + // Ephemeral keypair algorithm to generate per connection + keyType: text("key_type"), + // When true the profile is visible/usable by all users on the server + shared: boolean("shared").notNull().default(false), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +// Per-user cache of the ephemeral SSH private key + Vault-signed certificate. +// Transient: rows live only until the certificate expires. Secret fields are +// encrypted under the user's data-encryption key (see field-crypto.ts). +export const vaultTokens = pgTable( + "vault_tokens", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + profileId: integer("profile_id") + .notNull() + .references(() => vaultProfiles.id, { onDelete: "cascade" }), + + sshCert: text("ssh_cert").notNull(), + privateKey: text("private_key").notNull(), + + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: varchar("expires_at", { length: 255 }).notNull(), + lastUsed: text("last_used"), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [uniqueIndex("idx_vault_tokens_user_profile").on(table.userId, table.profileId)], +); + +export const apiKeys = pgTable( + "api_keys", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + tokenHash: text("token_hash").notNull(), + tokenPrefix: text("token_prefix").notNull(), + createdAt: varchar("created_at", { length: 255 }).notNull().default(sql`CURRENT_TIMESTAMP`), + expiresAt: varchar("expires_at", { length: 255 }), + lastUsedAt: text("last_used_at"), + isActive: boolean("is_active").notNull().default(true), + }, + (table) => [index("idx_api_keys_user_id").on(table.userId)], +); + +export const userOpenTabs = pgTable( + "user_open_tabs", + { + id: varchar("id", { length: 255 }).primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tabType: text("tab_type").notNull(), + hostId: integer("host_id").references(() => hosts.id, { + onDelete: "cascade", + }), + label: varchar("label", { length: 255 }).notNull(), + tabOrder: integer("tab_order").notNull().default(0), + backendSessionId: text("backend_session_id"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_user_open_tabs_user_id").on(table.userId)], +); + +export const userPreferences = pgTable("user_preferences", { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + reopenTabsOnLogin: boolean("reopen_tabs_on_login") + .notNull() + .default(false), + theme: text("theme"), + fontSize: text("font_size"), + accentColor: text("accent_color"), + language: text("language"), + storageMode: text("storage_mode"), + commandAutocomplete: boolean("command_autocomplete"), + commandPaletteEnabled: boolean("command_palette_enabled"), + showHostTags: boolean("show_host_tags"), + hostTrayOnClick: boolean("host_tray_on_click"), + pinAppRail: boolean("pin_app_rail"), + expandAppRailOnHover: boolean("expand_app_rail_on_hover"), + foldersCollapsed: boolean("folders_collapsed"), + confirmSnippetExecution: boolean("confirm_snippet_execution"), + disableUpdateCheck: boolean("disable_update_check"), + confirmTabClose: boolean("confirm_tab_close"), + hiddenRailTabs: text("hidden_rail_tabs"), + // null means the user has not been asked yet; the assistant stays hidden + // until this is explicitly true and the admin global is on. + aiAssistantEnabled: boolean("ai_assistant_enabled"), + // Opt-in to letting the assistant run allowlisted read-only diagnostics + // without a per-command approval click. + aiReadOnlyCommands: boolean("ai_read_only_commands"), + compactHostView: boolean("compact_host_view"), + statusColorScheme: text("status_color_scheme"), + customThemes: text("custom_themes"), + customKeybindings: text("custom_keybindings"), + terminalDefaults: text("terminal_defaults"), + rdpDefaults: text("rdp_defaults"), + terminalMacros: text("terminal_macros"), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const hostMetricsPreferences = pgTable( + "host_metrics_preferences", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + // JSON-encoded HostMetricsLayout. Layout has no secrets, so it is stored as + // plain JSON (no field-level encryption). + layout: text("layout").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // One layout per user per host. Enforced in production since the inline DDL + // creates it, but it was never declared here, so the generated Postgres and + // MySQL schemas lacked it โ€” and the upsert has nothing to conflict on. + (table) => [ + uniqueIndex("idx_host_metrics_prefs_user_host").on(table.userId, table.hostId), + ], +); + +export const proxmoxStatsPreferences = pgTable( + "proxmox_stats_preferences", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }).notNull().references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id").notNull().references(() => hosts.id, { onDelete: "cascade" }), + // JSON-encoded ProxmoxStatsLayout. Layout has no secrets, so it is stored as + // plain JSON (no field-level encryption), same convention as hostMetricsPreferences.layout. + layout: text("layout").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("idx_proxmox_stats_prefs_user_host").on(table.userId, table.hostId), + ], +); + +export const hostSidebarPreferences = pgTable("host_sidebar_preferences", { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded HostSidebarPreferences. No secrets in this blob, stored as + // plain JSON like hostMetricsPreferences.layout. + data: text("data").notNull(), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const credentialSidebarPreferences = pgTable( + "credential_sidebar_preferences", + { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded CredentialSidebarPreferences. No secrets in this blob, + // same convention as hostSidebarPreferences.data. + data: text("data").notNull(), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, +); + +export const uiPreferences = pgTable("ui_preferences", { + userId: varchar("user_id", { length: 255 }) + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded UiPreferences (preset + per-area overrides + onboarding + // state). No secrets in this blob, same convention as + // hostSidebarPreferences.data. + data: text("data").notNull(), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const hostHealthChecks = pgTable( + "host_health_checks", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + // JSON array of { id, name, type: "tcp"|"http", target, port, path } + checks: text("checks").notNull(), + intervalSeconds: integer("interval_seconds").notNull().default(300), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Same as above: one set of checks per user per host. + (table) => [ + uniqueIndex("idx_host_health_checks_user_host").on(table.userId, table.hostId), + ], +); + +export const hostHealthHistory = pgTable("host_health_history", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + checkId: text("check_id").notNull(), + ts: text("ts").notNull().default(sql`CURRENT_TIMESTAMP`), + ok: boolean("ok").notNull(), + latencyMs: integer("latency_ms"), + detail: text("detail"), +}); + +export const dashboardServiceLinks = pgTable("dashboard_service_links", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + label: varchar("label", { length: 255 }).notNull(), + url: text("url").notNull(), + order: integer("order").notNull().default(0), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +// --- termix-id begin --- +// A user claims a unique public handle. Their published SSH public keys are +// served at an unauthenticated resolver endpoint in authorized_keys format, +// so any server can be provisioned with `curl /termix-id/u/ >> ~/.ssh/authorized_keys`. +export const termixIdentities = pgTable("termix_identities", { + id: serial("id").primaryKey(), + // One Termix ID per user โ€” enforced in schema, not just in code. + userId: varchar("user_id", { length: 255 }) + .notNull() + .unique() + .references(() => users.id, { onDelete: "cascade" }), + handle: varchar("handle", { length: 255 }).notNull().unique(), + description: text("description"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const termixIdentityKeys = pgTable("termix_identity_keys", { + id: serial("id").primaryKey(), + identityId: integer("identity_id") + .notNull() + .references(() => termixIdentities.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // Public keys are non-secret, so they are stored in plaintext (no field-level + // encryption). This is what lets the unauthenticated resolver serve them. + publicKey: text("public_key").notNull(), + // Raw algorithm token (e.g. "ssh-ed25519"), and a normalized group used for + // the / resolver filter (RSA / ED25519 / ECDSA / ...). + keyType: text("key_type").notNull(), + algorithm: text("algorithm").notNull(), + label: varchar("label", { length: 255 }), + comment: text("comment"), + // "manual" (pasted) or "credential" (imported from an ssh_credentials entry). + source: text("source").notNull().default("manual"), + credentialId: integer("credential_id").references(() => sshCredentials.id, { + onDelete: "set null", + }), + enabled: boolean("enabled").notNull().default(true), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); +// Per-identity certificate authority. Servers that trust this CA (via +// TrustedUserCAKeys / @cert-authority) accept any user certificate it signs, +// giving central revocation (rotate the CA) and expiry (cert validity). +export const termixIdentityCa = pgTable("termix_identity_ca", { + id: serial("id").primaryKey(), + identityId: integer("identity_id") + .notNull() + .unique() + .references(() => termixIdentities.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // CA public key (plaintext โ€” it is published); CA private key is field-encrypted. + publicKey: text("public_key").notNull(), + privateKey: text("private_key").notNull(), + validityDays: integer("validity_days").notNull().default(90), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); +// --- termix-id end --- + +// --- tmux-monitor begin --- +export const tmuxSessionTags = pgTable("tmux_session_tags", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + sessionName: text("session_name").notNull(), + tag: text("tag").notNull(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); +// --- tmux-monitor end --- + +// --- metrics-history begin --- +export const hostMetricsHistory = pgTable("host_metrics_history", { + id: serial("id").primaryKey(), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + ts: text("ts") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + cpuPercent: doublePrecision("cpu_percent"), + memPercent: doublePrecision("mem_percent"), + diskPercent: doublePrecision("disk_percent"), + netRxBytes: integer("net_rx_bytes"), + netTxBytes: integer("net_tx_bytes"), +}); +// --- metrics-history end --- + +// --- proxmox-node-history begin --- +export const proxmoxNodeHistory = pgTable("proxmox_node_history", { + id: serial("id").primaryKey(), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + ts: text("ts") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + cpuPercent: doublePrecision("cpu_percent"), + memPercent: doublePrecision("mem_percent"), + diskPercent: doublePrecision("disk_percent"), + netRxBytes: integer("net_rx_bytes"), + netTxBytes: integer("net_tx_bytes"), +}); +// --- proxmox-node-history end --- + +// --- alerts begin --- +export const alertRules = pgTable("alert_rules", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id").references(() => hosts.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + enabled: boolean("enabled").notNull().default(true), + triggerType: text("trigger_type").notNull(), + thresholdValue: doublePrecision("threshold_value"), + thresholdDurationSeconds: integer("threshold_duration_seconds"), + cooldownMinutes: integer("cooldown_minutes").notNull().default(15), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const notificationChannels = pgTable("notification_channels", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + type: text("type").notNull(), + config: text("config").notNull(), + enabled: boolean("enabled").notNull().default(true), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const alertRuleChannels = pgTable("alert_rule_channels", { + id: serial("id").primaryKey(), + ruleId: integer("rule_id") + .notNull() + .references(() => alertRules.id, { onDelete: "cascade" }), + channelId: integer("channel_id") + .notNull() + .references(() => notificationChannels.id, { onDelete: "cascade" }), +}); + +export const alertFirings = pgTable( + "alert_firings", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + ruleId: integer("rule_id") + .notNull() + .references(() => alertRules.id, { onDelete: "cascade" }), + hostId: integer("host_id").notNull(), + hostName: text("host_name").notNull(), + firedAt: varchar("fired_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + resolvedAt: text("resolved_at"), + value: doublePrecision("value"), + message: text("message").notNull(), + severity: text("severity").notNull().default("warning"), + acknowledged: boolean("acknowledged") + .notNull() + .default(false), + }, + // A rule's history is read newest-first; host_id is filtered on its own. + (table) => [ + index("idx_alert_firings_rule").on(table.ruleId, table.firedAt), + index("idx_alert_firings_host").on(table.hostId), + ], +); +// --- alerts end --- + +// --- automations begin --- +export const automations = pgTable( + "automations", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + enabled: boolean("enabled").notNull().default(true), + // The whole trigger + steps graph, shaped by AutomationDefinition. Read and + // written as a unit, never queried by its inner structure. + definition: text("definition").notNull(), + definitionVersion: integer("definition_version").notNull().default(1), + concurrencyPolicy: text("concurrency_policy").notNull().default("skip"), + maxRunSeconds: integer("max_run_seconds").notNull().default(300), + dryRun: boolean("dry_run").notNull().default(false), + lastRunAt: text("last_run_at"), + lastRunStatus: text("last_run_status"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // The scheduler sweeps enabled automations for one user at a time. + (table) => [index("idx_automations_user").on(table.userId, table.enabled)], +); + +/** + * Durable per-target trigger state. state_key scopes a trigger to what it is + * actually watching ("", ":/data", ":"), so + * a sustained-breach window can track one filesystem rather than a whole host. + * Living in the database rather than memory means cooldowns and dwell windows + * survive a restart. + */ +export const automationTriggerState = pgTable( + "automation_trigger_state", + { + id: serial("id").primaryKey(), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + stateKey: varchar("state_key", { length: 255 }).notNull(), + breachStartedAt: text("breach_started_at"), + lastFiredAt: text("last_fired_at"), + lastValue: doublePrecision("last_value"), + lastObservedState: text("last_observed_state"), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("idx_automation_trigger_state_key").on( + table.automationId, + table.stateKey, + ), + ], +); + +export const automationSchedules = pgTable( + "automation_schedules", + { + id: serial("id").primaryKey(), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + cron: text("cron"), + intervalSeconds: integer("interval_seconds"), + timezone: text("timezone"), + nextDueAt: varchar("next_due_at", { length: 255 }), + lastTickAt: text("last_tick_at"), + }, + (table) => [ + uniqueIndex("idx_automation_schedules_automation").on(table.automationId), + index("idx_automation_schedules_due").on(table.nextDueAt), + ], +); + +export const automationRuns = pgTable( + "automation_runs", + { + id: serial("id").primaryKey(), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + triggerType: text("trigger_type").notNull(), + triggerContext: text("trigger_context"), + status: varchar("status", { length: 255 }).notNull(), + startedAt: varchar("started_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + finishedAt: text("finished_at"), + durationMs: integer("duration_ms"), + error: text("error"), + dryRun: boolean("dry_run").notNull().default(false), + // Set when one automation invoked another, so a chain can be traced. + parentRunId: integer("parent_run_id"), + }, + (table) => [ + index("idx_automation_runs_automation").on( + table.automationId, + table.startedAt, + ), + index("idx_automation_runs_user").on(table.userId, table.startedAt), + ], +); + +export const automationRunSteps = pgTable( + "automation_run_steps", + { + id: serial("id").primaryKey(), + runId: integer("run_id") + .notNull() + .references(() => automationRuns.id, { onDelete: "cascade" }), + stepIndex: integer("step_index").notNull(), + stepId: text("step_id").notNull(), + stepType: text("step_type").notNull(), + status: varchar("status", { length: 255 }).notNull(), + startedAt: varchar("started_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + finishedAt: text("finished_at"), + output: text("output"), + error: text("error"), + truncated: boolean("truncated") + .notNull() + .default(false), + }, + (table) => [ + index("idx_automation_run_steps_run").on(table.runId, table.stepIndex), + ], +); + +export const automationChannels = pgTable( + "automation_channels", + { + id: serial("id").primaryKey(), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + channelId: integer("channel_id") + .notNull() + .references(() => notificationChannels.id, { onDelete: "cascade" }), + }, + (table) => [ + uniqueIndex("idx_automation_channels_pair").on( + table.automationId, + table.channelId, + ), + ], +); +// --- automations end --- + +// --- homepage begin --- +export const homepageItems = pgTable( + "homepage_items", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + typeId: text("type_id").notNull(), + title: text("title"), + config: text("config").notNull().default("{}"), + folderId: integer("folder_id"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_homepage_items_user_id").on(table.userId)], +); + +export const homepageLayouts = pgTable("homepage_layouts", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .unique() + .references(() => users.id, { onDelete: "cascade" }), + // JSON: { entries: HomepageLayoutEntry[], pan: {x,y}, zoom: number } + layout: text("layout").notNull().default("{}"), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); +// --- homepage end --- + +// --- fleets begin --- +export const fleets = pgTable("fleets", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + color: text("color"), + icon: text("icon"), + // JSON array of { tag: string } rules, unioned with static fleetMembers at + // resolution time. Kept to tag-equality matching for v1. + tagRules: text("tag_rules"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const fleetMembers = pgTable( + "fleet_members", + { + id: serial("id").primaryKey(), + fleetId: integer("fleet_id") + .notNull() + .references(() => fleets.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + addedAt: text("added_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // fleet_id leads the unique pair, so listing a fleet's hosts is already + // served. Finding the fleets a host belongs to starts from host_id. + (table) => [ + uniqueIndex("idx_fleet_members_fleet_host").on(table.fleetId, table.hostId), + index("idx_fleet_members_host").on(table.hostId), + ], +); + +// Latest-only inventory snapshot per host, overwritten on each refresh - no +// historical log, matching the "latest snapshot only" scope decision. +export const fleetInventory = pgTable( + "fleet_inventory", + { + id: serial("id").primaryKey(), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + osPrettyName: text("os_pretty_name"), + kernel: text("kernel"), + architecture: text("architecture"), + hostname: text("hostname"), + uptimeSeconds: integer("uptime_seconds"), + ip: text("ip"), + packageManager: text("package_manager"), + collectedAt: text("collected_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // host_id leads the unique pair; a user's whole inventory is read by user_id. + (table) => [ + uniqueIndex("idx_fleet_inventory_host").on(table.hostId, table.userId), + index("idx_fleet_inventory_user").on(table.userId), + ], +); +// --- fleets end --- + +// --- workspaces begin --- +export const userWorkspaces = pgTable( + "user_workspaces", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + color: text("color"), + icon: text("icon"), + // "manual" | "last_session" - exactly one last_session row per user. + kind: text("kind").notNull().default("manual"), + isDefault: boolean("is_default") + .notNull() + .default(false), + // JSON-encoded WorkspacePayload: tabs, splitMode, paneTabIds, rowSizes, rowColSizes + payload: text("payload").notNull().default("{}"), + syncId: varchar("sync_id", { length: 255 }).unique(), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + lastUsedAt: text("last_used_at"), + }, + (table) => [index("idx_user_workspaces_user_id").on(table.userId)], +); +// --- workspaces end --- + +// --- sync begin --- +// Records a delete for a synced entity type so the other side of a sync +// pair (embedded desktop backend <-> connected remote server) learns about +// the deletion instead of re-creating the row on its next pull. +export const syncTombstones = pgTable("sync_tombstones", { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + entityType: text("entity_type").notNull(), + syncId: varchar("sync_id", { length: 255 }).notNull(), + deletedAt: text("deleted_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); +// --- sync end --- + +// --- ai begin --- +/** + * A user's connection to one AI provider. api_key is encrypted at rest via + * FieldCrypto; it is never returned to the frontend, which only ever sees + * api_key_prefix for display. + */ +export const aiProviders = pgTable( + "ai_providers", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // ollama | anthropic | openai | gemini | openai_compatible + providerType: text("provider_type").notNull(), + label: varchar("label", { length: 255 }).notNull(), + // Required for ollama and openai_compatible, optional elsewhere. + baseUrl: text("base_url"), + apiKey: text("api_key"), + // First few characters, kept in the clear so the UI can identify a key. + apiKeyPrefix: text("api_key_prefix"), + defaultModel: text("default_model"), + enabled: boolean("enabled").notNull().default(true), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("idx_ai_providers_user_label").on(table.userId, table.label), + ], +); + +export const aiConversations = pgTable( + "ai_conversations", + { + id: serial("id").primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + title: text("title"), + providerId: integer("provider_id"), + model: text("model"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: varchar("updated_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ai_conversations_user").on(table.userId, table.updatedAt), + ], +); + +export const aiMessages = pgTable( + "ai_messages", + { + id: serial("id").primaryKey(), + conversationId: integer("conversation_id") + .notNull() + .references(() => aiConversations.id, { onDelete: "cascade" }), + // user | assistant | tool + role: text("role").notNull(), + content: text("content").notNull().default(""), + // Serialized tool calls and their results for this turn. + toolCalls: text("tool_calls"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ai_messages_conversation").on( + table.conversationId, + table.createdAt, + ), + ], +); + +/** + * A change the assistant wants to make. Nothing here has been applied: the + * payload is re-validated against the tool schema at apply time and only then + * dispatched through the same repository logic a human action uses. + */ +export const aiProposals = pgTable( + "ai_proposals", + { + id: serial("id").primaryKey(), + conversationId: integer("conversation_id") + .notNull() + .references(() => aiConversations.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // The propose_* tool name that produced this. + kind: text("kind").notNull(), + summary: text("summary"), + payload: text("payload").notNull().default("{}"), + // pending | applied | rejected | expired + status: varchar("status", { length: 255 }).notNull().default("pending"), + appliedAt: text("applied_at"), + resultSummary: text("result_summary"), + createdAt: varchar("created_at", { length: 255 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ai_proposals_user").on(table.userId, table.status), + index("idx_ai_proposals_conversation").on(table.conversationId), + ], +); +// --- ai end --- diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 6e0a305..6a15e34 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -1,4 +1,12 @@ -import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core"; +import { + sqliteTable, + text, + integer, + real, + index, + uniqueIndex, + type AnySQLiteColumn, +} from "drizzle-orm/sqlite-core"; import { sql } from "drizzle-orm"; export const users = sqliteTable("users", { @@ -53,42 +61,54 @@ export const ssoProviders = sqliteTable("sso_providers", { .default(sql`CURRENT_TIMESTAMP`), }); -export const sessions = sqliteTable("sessions", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - jwtToken: text("jwt_token").notNull(), - deviceType: text("device_type").notNull(), - deviceInfo: text("device_info").notNull(), - oidcSub: text("oidc_sub"), - oidcSid: text("oidc_sid"), - ssoProviderId: integer("sso_provider_id"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - expiresAt: text("expires_at").notNull(), - lastActiveAt: text("last_active_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const sessions = sqliteTable( + "sessions", + { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + jwtToken: text("jwt_token").notNull(), + deviceType: text("device_type").notNull(), + deviceInfo: text("device_info").notNull(), + oidcSub: text("oidc_sub"), + oidcSid: text("oidc_sid"), + ssoProviderId: integer("sso_provider_id"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: text("expires_at").notNull(), + lastActiveAt: text("last_active_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Listing a user's devices, and the startup sweep of expired rows. + (table) => [ + index("idx_sessions_user_id").on(table.userId), + index("idx_sessions_expires_at").on(table.expiresAt), + ], +); -export const trustedDevices = sqliteTable("trusted_devices", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - deviceFingerprint: text("device_fingerprint").notNull(), - deviceType: text("device_type").notNull(), - deviceInfo: text("device_info").notNull(), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - expiresAt: text("expires_at").notNull(), - lastUsedAt: text("last_used_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const trustedDevices = sqliteTable( + "trusted_devices", + { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + deviceFingerprint: text("device_fingerprint").notNull(), + deviceType: text("device_type").notNull(), + deviceInfo: text("device_info").notNull(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: text("expires_at").notNull(), + lastUsedAt: text("last_used_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_trusted_devices_user_id").on(table.userId)], +); export const webauthnCredentials = sqliteTable("webauthn_credentials", { id: text("id").primaryKey(), @@ -109,228 +129,303 @@ export const webauthnCredentials = sqliteTable("webauthn_credentials", { lastUsedAt: text("last_used_at"), }); -export const hosts = sqliteTable("ssh_data", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - connectionType: text("connection_type").notNull().default("ssh"), - name: text("name"), - ip: text("ip").notNull(), - port: integer("port").notNull(), - username: text("username").notNull(), - folder: text("folder"), - tags: text("tags"), - pin: integer("pin", { mode: "boolean" }).notNull().default(false), - authType: text("auth_type").notNull(), - useWarpgate: integer("use_warpgate", { mode: "boolean" }).notNull().default(false), - forceKeyboardInteractive: text("force_keyboard_interactive"), +export const hosts = sqliteTable( + "ssh_data", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + connectionType: text("connection_type").notNull().default("ssh"), + name: text("name"), + ip: text("ip").notNull(), + port: integer("port").notNull(), + username: text("username").notNull(), + folder: text("folder"), + // Sub-host nesting: a host acting as an organizational parent for other + // hosts, mutually exclusive with folder (see host route validation). + parentHostId: integer("parent_host_id").references( + (): AnySQLiteColumn => hosts.id, + { onDelete: "set null" }, + ), + tags: text("tags"), + pin: integer("pin", { mode: "boolean" }).notNull().default(false), + // Manual drag-to-reorder position within a folder. Null means the host has + // never been manually reordered; falls back to name sort in that case. + sortOrder: integer("sort_order"), + authType: text("auth_type").notNull(), + useWarpgate: integer("use_warpgate", { mode: "boolean" }).notNull().default(false), + shareSshAuth: integer("share_ssh_auth", { mode: "boolean" }) + .notNull() + .default(false), + forceKeyboardInteractive: text("force_keyboard_interactive"), - password: text("password"), - key: text("key", { length: 8192 }), - keyPassword: text("key_password"), - keyType: text("key_type"), - sudoPassword: text("sudo_password"), + password: text("password"), + key: text("key", { length: 8192 }), + keyPassword: text("key_password"), + keyType: text("key_type"), + sudoPassword: text("sudo_password"), - autostartPassword: text("autostart_password"), - autostartKey: text("autostart_key", { length: 8192 }), - autostartKeyPassword: text("autostart_key_password"), + autostartPassword: text("autostart_password"), + autostartKey: text("autostart_key", { length: 8192 }), + autostartKeyPassword: text("autostart_key_password"), - credentialId: integer("credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), - overrideCredentialUsername: integer("override_credential_username", { - mode: "boolean", - }), - // When authType is "vault", the host authenticates via a Vault SSH signer - // profile (shared settings, no secrets). The signing certificate is obtained - // per-user at connect time via an interactive Vault OIDC flow. - vaultProfileId: integer("vault_profile_id").references( - () => vaultProfiles.id, - { onDelete: "set null" }, - ), - enableTerminal: integer("enable_terminal", { mode: "boolean" }) - .notNull() - .default(true), - enableSessionLogging: integer("enable_session_logging", { mode: "boolean" }) - .notNull() - .default(true), - enableCommandHistory: integer("enable_command_history", { mode: "boolean" }) - .notNull() - .default(true), - enableTunnel: integer("enable_tunnel", { mode: "boolean" }) - .notNull() - .default(true), - tunnelConnections: text("tunnel_connections"), - jumpHosts: text("jump_hosts"), - enableFileManager: integer("enable_file_manager", { mode: "boolean" }) - .notNull() - .default(true), - scpLegacy: integer("scp_legacy", { mode: "boolean" }).notNull().default(false), - enableDocker: integer("enable_docker", { mode: "boolean" }) - .notNull() - .default(false), - enableTmuxMonitor: integer("enable_tmux_monitor", { mode: "boolean" }) - .notNull() - .default(false), - showTerminalInSidebar: integer("show_terminal_in_sidebar", { mode: "boolean" }) - .notNull() - .default(true), - showFileManagerInSidebar: integer("show_file_manager_in_sidebar", { mode: "boolean" }) - .notNull() - .default(false), - showTunnelInSidebar: integer("show_tunnel_in_sidebar", { mode: "boolean" }) - .notNull() - .default(false), - showDockerInSidebar: integer("show_docker_in_sidebar", { mode: "boolean" }) - .notNull() - .default(false), - showServerStatsInSidebar: integer("show_server_stats_in_sidebar", { mode: "boolean" }) - .notNull() - .default(false), - defaultPath: text("default_path"), - statsConfig: text("stats_config"), - dockerConfig: text("docker_config"), - enableProxmox: integer("enable_proxmox", { mode: "boolean" }) - .notNull() - .default(false), - proxmoxConfig: text("proxmox_config"), - terminalConfig: text("terminal_config"), - quickActions: text("quick_actions"), - notes: text("notes"), - enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true), - enableRdp: integer("enable_rdp", { mode: "boolean" }).notNull().default(false), - enableVnc: integer("enable_vnc", { mode: "boolean" }).notNull().default(false), - enableTelnet: integer("enable_telnet", { mode: "boolean" }).notNull().default(false), + credentialId: integer("credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + overrideCredentialUsername: integer("override_credential_username", { + mode: "boolean", + }), + // When authType is "vault", the host authenticates via a Vault SSH signer + // profile (shared settings, no secrets). The signing certificate is obtained + // per-user at connect time via an interactive Vault OIDC flow. + vaultProfileId: integer("vault_profile_id").references( + () => vaultProfiles.id, + { onDelete: "set null" }, + ), + enableTerminal: integer("enable_terminal", { mode: "boolean" }) + .notNull() + .default(true), + enableSessionLogging: integer("enable_session_logging", { mode: "boolean" }) + .notNull() + .default(true), + allowSessionSharing: integer("allow_session_sharing", { mode: "boolean" }) + .notNull() + .default(true), + enableCommandHistory: integer("enable_command_history", { mode: "boolean" }) + .notNull() + .default(true), + enableTunnel: integer("enable_tunnel", { mode: "boolean" }) + .notNull() + .default(true), + tunnelConnections: text("tunnel_connections"), + jumpHosts: text("jump_hosts"), + enableFileManager: integer("enable_file_manager", { mode: "boolean" }) + .notNull() + .default(true), + scpLegacy: integer("scp_legacy", { mode: "boolean" }).notNull().default(false), + enableDocker: integer("enable_docker", { mode: "boolean" }) + .notNull() + .default(false), + enableTmuxMonitor: integer("enable_tmux_monitor", { mode: "boolean" }) + .notNull() + .default(false), + enableTerminalToolbar: integer("enable_terminal_toolbar", { mode: "boolean" }) + .notNull() + .default(true), + showTerminalInSidebar: integer("show_terminal_in_sidebar", { mode: "boolean" }) + .notNull() + .default(true), + showFileManagerInSidebar: integer("show_file_manager_in_sidebar", { mode: "boolean" }) + .notNull() + .default(false), + showTunnelInSidebar: integer("show_tunnel_in_sidebar", { mode: "boolean" }) + .notNull() + .default(false), + showDockerInSidebar: integer("show_docker_in_sidebar", { mode: "boolean" }) + .notNull() + .default(false), + showServerStatsInSidebar: integer("show_server_stats_in_sidebar", { mode: "boolean" }) + .notNull() + .default(false), + defaultPath: text("default_path"), + statsConfig: text("stats_config"), + dockerConfig: text("docker_config"), + enableProxmox: integer("enable_proxmox", { mode: "boolean" }) + .notNull() + .default(false), + proxmoxConfig: text("proxmox_config"), + enableProxmoxStats: integer("enable_proxmox_stats", { mode: "boolean" }) + .notNull() + .default(false), + proxmoxStatsConfig: text("proxmox_stats_config"), + terminalConfig: text("terminal_config"), + quickActions: text("quick_actions"), + notes: text("notes"), + enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true), + enableRdp: integer("enable_rdp", { mode: "boolean" }).notNull().default(false), + enableVnc: integer("enable_vnc", { mode: "boolean" }).notNull().default(false), + enableTelnet: integer("enable_telnet", { mode: "boolean" }).notNull().default(false), - sshPort: integer("ssh_port").default(22), - rdpPort: integer("rdp_port").default(3389), - vncPort: integer("vnc_port").default(5900), - telnetPort: integer("telnet_port").default(23), + sshPort: integer("ssh_port").default(22), + rdpPort: integer("rdp_port").default(3389), + vncPort: integer("vnc_port").default(5900), + telnetPort: integer("telnet_port").default(23), - rdpCredentialId: integer("rdp_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), - rdpUser: text("rdp_user"), - rdpPassword: text("rdp_password"), - rdpDomain: text("rdp_domain"), - rdpSecurity: text("rdp_security"), - rdpIgnoreCert: integer("rdp_ignore_cert", { mode: "boolean" }).default(false), + rdpCredentialId: integer("rdp_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + rdpUser: text("rdp_user"), + rdpPassword: text("rdp_password"), + rdpDomain: text("rdp_domain"), + rdpSecurity: text("rdp_security"), + rdpIgnoreCert: integer("rdp_ignore_cert", { mode: "boolean" }).default(false), - vncCredentialId: integer("vnc_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), - vncPassword: text("vnc_password"), - vncUser: text("vnc_user"), + vncCredentialId: integer("vnc_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + vncPassword: text("vnc_password"), + vncUser: text("vnc_user"), - telnetUser: text("telnet_user"), - telnetPassword: text("telnet_password"), - telnetCredentialId: integer("telnet_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), + telnetUser: text("telnet_user"), + telnetPassword: text("telnet_password"), + telnetCredentialId: integer("telnet_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }), - rdpAuthType: text("rdp_auth_type"), - vncAuthType: text("vnc_auth_type"), - telnetAuthType: text("telnet_auth_type"), + rdpAuthType: text("rdp_auth_type"), + vncAuthType: text("vnc_auth_type"), + telnetAuthType: text("telnet_auth_type"), - domain: text("domain"), - security: text("security"), - ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false), - guacamoleConfig: text("guacamole_config"), + domain: text("domain"), + security: text("security"), + ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false), + guacamoleConfig: text("guacamole_config"), - useSocks5: integer("use_socks5", { mode: "boolean" }), - socks5Host: text("socks5_host"), - socks5Port: integer("socks5_port"), - socks5Username: text("socks5_username"), - socks5Password: text("socks5_password"), - socks5ProxyChain: text("socks5_proxy_chain"), + useSocks5: integer("use_socks5", { mode: "boolean" }), + socks5Host: text("socks5_host"), + socks5Port: integer("socks5_port"), + socks5Username: text("socks5_username"), + socks5Password: text("socks5_password"), + socks5ProxyChain: text("socks5_proxy_chain"), - macAddress: text("mac_address"), - wolBroadcastAddress: text("wol_broadcast_address"), - portKnockSequence: text("port_knock_sequence"), + // null = use the desktop app's global default; "local" | "remote" pins + // this specific host's SSH/Docker-console/Serial connections to originate + // from the embedded local backend or a connected remote sync server. + // Ignored for rdp/vnc/telnet, which always require the remote server. + connectionOrigin: text("connection_origin"), - hostKeyFingerprint: text("host_key_fingerprint"), - hostKeyType: text("host_key_type"), - hostKeyAlgorithm: text("host_key_algorithm").default("sha256"), - hostKeyFirstSeen: text("host_key_first_seen"), - hostKeyLastVerified: text("host_key_last_verified"), - hostKeyChangedCount: integer("host_key_changed_count").default(0), + macAddress: text("mac_address"), + wolBroadcastAddress: text("wol_broadcast_address"), + portKnockSequence: text("port_knock_sequence"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); + hostKeyFingerprint: text("host_key_fingerprint"), + hostKeyType: text("host_key_type"), + hostKeyAlgorithm: text("host_key_algorithm").default("sha256"), + hostKeyFirstSeen: text("host_key_first_seen"), + hostKeyLastVerified: text("host_key_last_verified"), + hostKeyChangedCount: integer("host_key_changed_count").default(0), -export const fileManagerRecent = sqliteTable("file_manager_recent", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - name: text("name").notNull(), - path: text("path").notNull(), - lastOpened: text("last_opened") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); + // Stable identity used to match this row across two independently-seeded + // databases (the embedded backend and a connected remote server) during + // sync -- local autoincrement ids collide across instances. + syncId: text("sync_id").unique(), -export const fileManagerPinned = sqliteTable("file_manager_pinned", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - name: text("name").notNull(), - path: text("path").notNull(), - pinnedAt: text("pinned_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Every host read is scoped by owner, so user_id carries the host list. + // + // `folder` is deliberately not indexed: on Postgres/MySQL an indexed text + // column is generated as varchar(255), and folder holds a joined nested path + // with no length cap, so indexing it would truncate deep hierarchies. + (table) => [ + index("idx_ssh_data_user_id").on(table.userId), + index("idx_ssh_data_parent_host").on(table.parentHostId), + index("idx_ssh_data_credential").on(table.credentialId), + ], +); -export const fileManagerShortcuts = sqliteTable("file_manager_shortcuts", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - name: text("name").notNull(), - path: text("path").notNull(), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const fileManagerRecent = sqliteTable( + "file_manager_recent", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: text("name").notNull(), + path: text("path").notNull(), + lastOpened: text("last_opened") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Every file manager surface is read for one user on one host at a time. + (table) => [ + index("idx_file_manager_recent_user").on(table.userId, table.hostId), + ], +); -export const transferRecent = sqliteTable("transfer_recent", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - sourceHostId: integer("source_host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - destHostId: integer("dest_host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - destPath: text("dest_path").notNull(), - destPathLabel: text("dest_path_label").notNull(), - lastUsed: text("last_used") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const fileManagerPinned = sqliteTable( + "file_manager_pinned", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: text("name").notNull(), + path: text("path").notNull(), + pinnedAt: text("pinned_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_file_manager_pinned_user").on(table.userId, table.hostId), + ], +); -export const dismissedAlerts = sqliteTable("dismissed_alerts", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - alertId: text("alert_id").notNull(), - dismissedAt: text("dismissed_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const fileManagerShortcuts = sqliteTable( + "file_manager_shortcuts", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + name: text("name").notNull(), + path: text("path").notNull(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_file_manager_shortcuts_user").on(table.userId, table.hostId), + ], +); -export const sshCredentials = sqliteTable("ssh_credentials", { +export const transferRecent = sqliteTable( + "transfer_recent", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + sourceHostId: integer("source_host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + destHostId: integer("dest_host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + destPath: text("dest_path").notNull(), + destPathLabel: text("dest_path_label").notNull(), + lastUsed: text("last_used") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_transfer_recent_user").on(table.userId)], +); + +export const dismissedAlerts = sqliteTable( + "dismissed_alerts", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + alertId: text("alert_id").notNull(), + dismissedAt: text("dismissed_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_dismissed_alerts_user_id").on(table.userId)], +); + +export const sshCredentials = sqliteTable( + "ssh_credentials", + { id: integer("id").primaryKey({ autoIncrement: true }), userId: text("user_id") .notNull() @@ -339,6 +434,11 @@ export const sshCredentials = sqliteTable("ssh_credentials", { description: text("description"), folder: text("folder"), tags: text("tags"), + pin: integer("pin", { mode: "boolean" }).notNull().default(false), + // Manual drag-to-reorder position within a folder. Null means the + // credential has never been manually reordered; falls back to name sort + // in that case, same convention as hosts.sortOrder. + sortOrder: integer("sort_order"), authType: text("auth_type").notNull(), username: text("username"), password: text("password"), @@ -354,48 +454,64 @@ export const sshCredentials = sqliteTable("ssh_credentials", { usageCount: integer("usage_count").notNull().default(0), lastUsed: text("last_used"), + syncId: text("sync_id").unique(), createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), updatedAt: text("updated_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), -}); + }, + (table) => [index("idx_ssh_credentials_user_id").on(table.userId)], +); -export const sshCredentialUsage = sqliteTable("ssh_credential_usage", { - id: integer("id").primaryKey({ autoIncrement: true }), - credentialId: integer("credential_id") - .notNull() - .references(() => sshCredentials.id, { onDelete: "cascade" }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - usedAt: text("used_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const sshCredentialUsage = sqliteTable( + "ssh_credential_usage", + { + id: integer("id").primaryKey({ autoIncrement: true }), + credentialId: integer("credential_id") + .notNull() + .references(() => sshCredentials.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + usedAt: text("used_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ssh_credential_usage_credential").on(table.credentialId), + index("idx_ssh_credential_usage_user").on(table.userId), + ], +); -export const snippets = sqliteTable("snippets", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - name: text("name").notNull(), - content: text("content").notNull(), - description: text("description"), - folder: text("folder"), - order: integer("order").notNull().default(0), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - hostFilter: text("host_filter"), -}); +export const snippets = sqliteTable( + "snippets", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + content: text("content").notNull(), + description: text("description"), + folder: text("folder"), + order: integer("order").notNull().default(0), + syncId: text("sync_id").unique(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + hostFilter: text("host_filter"), + isNote: integer("is_note", { mode: "boolean" }).notNull().default(false), + }, + (table) => [index("idx_snippets_user_id").on(table.userId)], +); export const snippetFolders = sqliteTable("snippet_folders", { id: integer("id").primaryKey({ autoIncrement: true }), @@ -405,6 +521,7 @@ export const snippetFolders = sqliteTable("snippet_folders", { name: text("name").notNull(), color: text("color"), icon: text("icon"), + syncId: text("sync_id").unique(), createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), @@ -430,74 +547,107 @@ export const c2sTunnelPresets = sqliteTable("c2s_tunnel_presets", { .default(sql`CURRENT_TIMESTAMP`), }); -export const snippetAccess = sqliteTable("snippet_access", { - id: integer("id").primaryKey({ autoIncrement: true }), - snippetId: integer("snippet_id") - .notNull() - .references(() => snippets.id, { onDelete: "cascade" }), +export const snippetAccess = sqliteTable( + "snippet_access", + { + id: integer("id").primaryKey({ autoIncrement: true }), + snippetId: integer("snippet_id") + .notNull() + .references(() => snippets.id, { onDelete: "cascade" }), - userId: text("user_id").references(() => users.id, { onDelete: "cascade" }), - roleId: integer("role_id").references(() => roles.id, { - onDelete: "cascade", - }), + userId: text("user_id").references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id").references(() => roles.id, { + onDelete: "cascade", + }), - grantedBy: text("granted_by") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), + grantedBy: text("granted_by") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), - permissionLevel: text("permission_level").notNull().default("view"), + permissionLevel: text("permission_level").notNull().default("view"), - expiresAt: text("expires_at"), + expiresAt: text("expires_at"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Same three lookup shapes as host_access: by grantee, by role, by snippet. + (table) => [ + index("idx_snippet_access_user_id").on(table.userId), + index("idx_snippet_access_snippet_id").on(table.snippetId), + index("idx_snippet_access_role_id").on(table.roleId), + ], +); -export const sshFolders = sqliteTable("ssh_folders", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - name: text("name").notNull(), - color: text("color"), - icon: text("icon"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const sshFolders = sqliteTable( + "ssh_folders", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + color: text("color"), + icon: text("icon"), + credentialId: integer("credential_id").references(() => sshCredentials.id, { + onDelete: "set null", + }), + // Manual drag-to-reorder position among sibling folders. Null falls back + // to name sort, same convention as hosts.sortOrder. + sortOrder: integer("sort_order"), + syncId: text("sync_id").unique(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_ssh_folders_user_id").on(table.userId)], +); -export const recentActivity = sqliteTable("recent_activity", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - type: text("type").notNull(), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - hostName: text("host_name"), - timestamp: text("timestamp") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const recentActivity = sqliteTable( + "recent_activity", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").notNull(), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + hostName: text("host_name"), + timestamp: text("timestamp") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Always read newest-first for one user, so timestamp follows user_id. + (table) => [ + index("idx_recent_activity_user_ts").on(table.userId, table.timestamp), + ], +); -export const commandHistory = sqliteTable("command_history", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - command: text("command").notNull(), - executedAt: text("executed_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const commandHistory = sqliteTable( + "command_history", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + command: text("command").notNull(), + executedAt: text("executed_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_command_history_user_host").on(table.userId, table.hostId), + ], +); export const networkTopology = sqliteTable("network_topology", { id: integer("id").primaryKey({ autoIncrement: true }), @@ -513,72 +663,122 @@ export const networkTopology = sqliteTable("network_topology", { .default(sql`CURRENT_TIMESTAMP`), }); -export const hostAccess = sqliteTable("host_access", { - id: integer("id").primaryKey({ autoIncrement: true }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), +export const hostAccess = sqliteTable( + "host_access", + { + id: integer("id").primaryKey({ autoIncrement: true }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), - userId: text("user_id") - .references(() => users.id, { onDelete: "cascade" }), - roleId: integer("role_id") - .references(() => roles.id, { onDelete: "cascade" }), + userId: text("user_id") + .references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id") + .references(() => roles.id, { onDelete: "cascade" }), - grantedBy: text("granted_by") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), + grantedBy: text("granted_by") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), - permissionLevel: text("permission_level") - .notNull() - .default("connect"), + permissionLevel: text("permission_level") + .notNull() + .default("connect"), - expiresAt: text("expires_at"), + expiresAt: text("expires_at"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - lastAccessedAt: text("last_accessed_at"), - accessCount: integer("access_count").notNull().default(0), - overrideCredentialId: integer("override_credential_id").references( - () => sshCredentials.id, - { onDelete: "set null" }, - ), -}); + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + lastAccessedAt: text("last_accessed_at"), + accessCount: integer("access_count").notNull().default(0), + }, + // Resolved on every host list request and every permission check, so all + // three lookup shapes (by grantee, by role, by host) need to be indexed. + (table) => [ + index("idx_host_access_user_id").on(table.userId), + index("idx_host_access_role_id").on(table.roleId), + index("idx_host_access_host_id").on(table.hostId), + index("idx_host_access_expires_at").on(table.expiresAt), + ], +); -export const sharedHostSecrets = sqliteTable("shared_host_secrets", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const sharedHostAuthOverrides = sqliteTable( + "shared_host_auth_overrides", + { + id: integer("id").primaryKey({ autoIncrement: true }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + protocol: text("protocol").notNull().default("ssh"), + credentialId: integer("credential_id") + .notNull() + .references(() => sshCredentials.id, { onDelete: "cascade" }), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("shared_host_auth_overrides_host_user_protocol_unique").on( + table.hostId, + table.userId, + table.protocol, + ), + ], +); - hostAccessId: integer("host_access_id") - .notNull() - .references(() => hostAccess.id, { onDelete: "cascade" }), +export const sharedHostSecrets = sqliteTable( + "shared_host_secrets", + { + id: integer("id").primaryKey({ autoIncrement: true }), - targetUserId: text("target_user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), + hostAccessId: integer("host_access_id") + .notNull() + .references(() => hostAccess.id, { onDelete: "cascade" }), - protocol: text("protocol").notNull().default("ssh"), - sourceType: text("source_type").notNull().default("credential"), + targetUserId: text("target_user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), - originalCredentialId: integer("original_credential_id").references( - () => sshCredentials.id, - { onDelete: "cascade" }, - ), + protocol: text("protocol").notNull().default("ssh"), + sourceType: text("source_type").notNull().default("credential"), - encryptedUsername: text("encrypted_username"), - encryptedAuthType: text("encrypted_auth_type"), - encryptedPassword: text("encrypted_password"), - encryptedKey: text("encrypted_key", { length: 16384 }), - encryptedKeyPassword: text("encrypted_key_password"), - encryptedKeyType: text("encrypted_key_type"), - encryptedDomain: text("encrypted_domain"), + originalCredentialId: integer("original_credential_id").references( + () => sshCredentials.id, + { onDelete: "cascade" }, + ), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); + encryptedUsername: text("encrypted_username"), + encryptedAuthType: text("encrypted_auth_type"), + encryptedPassword: text("encrypted_password"), + encryptedKey: text("encrypted_key", { length: 16384 }), + encryptedKeyPassword: text("encrypted_key_password"), + encryptedKeyType: text("encrypted_key_type"), + encryptedDomain: text("encrypted_domain"), + + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [ + uniqueIndex("idx_shared_host_secrets_scope").on( + table.hostAccessId, + table.targetUserId, + table.protocol, + ), + ], +); export const roles = sqliteTable("roles", { id: integer("id").primaryKey({ autoIncrement: true }), @@ -600,102 +800,211 @@ export const roles = sqliteTable("roles", { .default(sql`CURRENT_TIMESTAMP`), }); -export const userRoles = sqliteTable("user_roles", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - roleId: integer("role_id") - .notNull() - .references(() => roles.id, { onDelete: "cascade" }), +export const userRoles = sqliteTable( + "user_roles", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id") + .notNull() + .references(() => roles.id, { onDelete: "cascade" }), + + grantedBy: text("granted_by").references(() => users.id, { + onDelete: "set null", + }), + grantedAt: text("granted_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + // + // The unique pair already serves lookups by user, since user_id leads it. + // Listing a role's members starts from role_id, which it cannot serve. + (table) => [ + uniqueIndex("idx_user_roles_user_role").on(table.userId, table.roleId), + index("idx_user_roles_role_id").on(table.roleId), + ], +); - grantedBy: text("granted_by").references(() => users.id, { - onDelete: "set null", - }), - grantedAt: text("granted_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const auditLogs = sqliteTable( + "audit_logs", + { + id: integer("id").primaryKey({ autoIncrement: true }), -export const auditLogs = sqliteTable("audit_logs", { - id: integer("id").primaryKey({ autoIncrement: true }), + // Nullable on purpose: the trail outlives the account, and username keeps the + // entry attributable once the reference is gone. + userId: text("user_id").references(() => users.id, { onDelete: "set null" }), + username: text("username").notNull(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - username: text("username").notNull(), + action: text("action").notNull(), + resourceType: text("resource_type").notNull(), + resourceId: text("resource_id"), + resourceName: text("resource_name"), - action: text("action").notNull(), - resourceType: text("resource_type").notNull(), - resourceId: text("resource_id"), - resourceName: text("resource_name"), + details: text("details"), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), - details: text("details"), - ipAddress: text("ip_address"), - userAgent: text("user_agent"), + success: integer("success", { mode: "boolean" }).notNull(), + errorMessage: text("error_message"), - success: integer("success", { mode: "boolean" }).notNull(), - errorMessage: text("error_message"), + timestamp: text("timestamp") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // This table only grows, and is always read newest-first with an optional + // filter. Each composite leads with the filtered column so the same index + // also satisfies the ORDER BY. + (table) => [ + index("idx_audit_logs_timestamp").on(table.timestamp), + index("idx_audit_logs_user_ts").on(table.userId, table.timestamp), + index("idx_audit_logs_action_ts").on(table.action, table.timestamp), + index("idx_audit_logs_resource_ts").on(table.resourceType, table.timestamp), + ], +); - timestamp: text("timestamp") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const sessionRecordings = sqliteTable( + "session_recordings", + { + id: integer("id").primaryKey({ autoIncrement: true }), -export const sessionRecordings = sqliteTable("session_recordings", { - id: integer("id").primaryKey({ autoIncrement: true }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + // Nullable on purpose: a recording is evidence about the host as much as the + // person, so it outlives the account. username keeps it attributable. + userId: text("user_id").references(() => users.id, { onDelete: "set null" }), + username: text("username"), + accessId: integer("access_id").references(() => hostAccess.id, { + onDelete: "set null", + }), + + startedAt: text("started_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + endedAt: text("ended_at"), + duration: integer("duration"), + + commands: text("commands"), + dangerousActions: text("dangerous_actions"), + + recordingPath: text("recording_path"), + protocol: text("protocol").notNull().default("ssh"), + format: text("format").notNull().default("text"), + + terminatedByOwner: integer("terminated_by_owner", { + mode: "boolean", + }).default(false), + terminationReason: text("termination_reason"), + }, + // Listed newest-first per user, and audited per host. + (table) => [ + index("idx_session_recordings_user_started").on( + table.userId, + table.startedAt, + ), + index("idx_session_recordings_host").on(table.hostId), + ], +); + +export const sessionShares = sqliteTable( + "session_shares", + { + id: text("id").primaryKey(), hostId: integer("host_id") .notNull() .references(() => hosts.id, { onDelete: "cascade" }), - userId: text("user_id") + ownerUserId: text("owner_user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), - accessId: integer("access_id").references(() => hostAccess.id, { - onDelete: "set null", + + protocol: text("protocol").notNull(), + + // Live-session binding: TerminalSessionManager's session.id for SSH, or + // guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB + // row (process-local, in-memory) so this intentionally has no FK. + sessionId: text("session_id").notNull(), + tabInstanceId: text("tab_instance_id"), + + shareType: text("share_type").notNull(), // "link" | "user" + targetUserId: text("target_user_id").references(() => users.id, { + onDelete: "cascade", }), + linkToken: text("link_token").unique(), - startedAt: text("started_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - endedAt: text("ended_at"), - duration: integer("duration"), - - commands: text("commands"), - dangerousActions: text("dangerous_actions"), - - recordingPath: text("recording_path"), - protocol: text("protocol").notNull().default("ssh"), - format: text("format").notNull().default("text"), - - terminatedByOwner: integer("terminated_by_owner", { mode: "boolean" }) - .default(false), - terminationReason: text("termination_reason"), -}); - -export const opksshTokens = sqliteTable("opkssh_tokens", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - - sshCert: text("ssh_cert", { length: 8192 }).notNull(), - privateKey: text("private_key", { length: 8192 }).notNull(), - - email: text("email"), - sub: text("sub"), - issuer: text("issuer"), - audience: text("audience"), + permissionLevel: text("permission_level").notNull().default("read-only"), createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), expiresAt: text("expires_at").notNull(), - lastUsed: text("last_used"), -}); + revokedAt: text("revoked_at"), + + lastJoinedAt: text("last_joined_at"), + joinCount: integer("join_count").notNull().default(0), + }, + // Resolved from the live session on join, and listed per host. + (table) => [ + index("idx_session_shares_session_id").on(table.sessionId), + index("idx_session_shares_host_id").on(table.hostId), + ], +); + +export const sessionShareParticipants = sqliteTable( + "session_share_participants", + { + id: integer("id").primaryKey({ autoIncrement: true }), + shareId: text("share_id") + .notNull() + .references(() => sessionShares.id, { onDelete: "cascade" }), + + userId: text("user_id").references(() => users.id, { + onDelete: "cascade", + }), + guestLabel: text("guest_label"), + + joinedAt: text("joined_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + leftAt: text("left_at"), + }, +); + +export const opksshTokens = sqliteTable( + "opkssh_tokens", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + + sshCert: text("ssh_cert", { length: 8192 }).notNull(), + privateKey: text("private_key", { length: 8192 }).notNull(), + + email: text("email"), + sub: text("sub"), + issuer: text("issuer"), + audience: text("audience"), + + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: text("expires_at").notNull(), + lastUsed: text("last_used"), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [uniqueIndex("idx_opkssh_tokens_user_host").on(table.userId, table.hostId)], +); // Vault SSH signer profiles. These hold ONLY non-secret connection settings and // are intended to be shared across users (shared === true makes a profile @@ -724,6 +1033,7 @@ export const vaultProfiles = sqliteTable("vault_profiles", { keyType: text("key_type"), // When true the profile is visible/usable by all users on the server shared: integer("shared", { mode: "boolean" }).notNull().default(false), + syncId: text("sync_id").unique(), createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), @@ -735,56 +1045,73 @@ export const vaultProfiles = sqliteTable("vault_profiles", { // Per-user cache of the ephemeral SSH private key + Vault-signed certificate. // Transient: rows live only until the certificate expires. Secret fields are // encrypted under the user's data-encryption key (see field-crypto.ts). -export const vaultTokens = sqliteTable("vault_tokens", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - profileId: integer("profile_id") - .notNull() - .references(() => vaultProfiles.id, { onDelete: "cascade" }), +export const vaultTokens = sqliteTable( + "vault_tokens", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + profileId: integer("profile_id") + .notNull() + .references(() => vaultProfiles.id, { onDelete: "cascade" }), + + sshCert: text("ssh_cert", { length: 8192 }).notNull(), + privateKey: text("private_key", { length: 8192 }).notNull(), + + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: text("expires_at").notNull(), + lastUsed: text("last_used"), + }, + // Declared inline in the production DDL as UNIQUE(...), but never here, + // so the generated Postgres and MySQL schemas allowed duplicates the + // SQLite deployment forbids โ€” and the upsert had nothing to conflict on. + (table) => [uniqueIndex("idx_vault_tokens_user_profile").on(table.userId, table.profileId)], +); - sshCert: text("ssh_cert", { length: 8192 }).notNull(), - privateKey: text("private_key", { length: 8192 }).notNull(), +export const apiKeys = sqliteTable( + "api_keys", + { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + tokenHash: text("token_hash").notNull(), + tokenPrefix: text("token_prefix").notNull(), + createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`), + expiresAt: text("expires_at"), + lastUsedAt: text("last_used_at"), + isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), + }, + (table) => [index("idx_api_keys_user_id").on(table.userId)], +); - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - expiresAt: text("expires_at").notNull(), - lastUsed: text("last_used"), -}); - -export const apiKeys = sqliteTable("api_keys", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - name: text("name").notNull(), - tokenHash: text("token_hash").notNull(), - tokenPrefix: text("token_prefix").notNull(), - createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`), - expiresAt: text("expires_at"), - lastUsedAt: text("last_used_at"), - isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), -}); - -export const userOpenTabs = sqliteTable("user_open_tabs", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - tabType: text("tab_type").notNull(), - hostId: integer("host_id").references(() => hosts.id, { onDelete: "cascade" }), - label: text("label").notNull(), - tabOrder: integer("tab_order").notNull().default(0), - backendSessionId: text("backend_session_id"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const userOpenTabs = sqliteTable( + "user_open_tabs", + { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tabType: text("tab_type").notNull(), + hostId: integer("host_id").references(() => hosts.id, { + onDelete: "cascade", + }), + label: text("label").notNull(), + tabOrder: integer("tab_order").notNull().default(0), + backendSessionId: text("backend_session_id"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_user_open_tabs_user_id").on(table.userId)], +); export const userPreferences = sqliteTable("user_preferences", { userId: text("user_id") @@ -811,14 +1138,27 @@ export const userPreferences = sqliteTable("user_preferences", { disableUpdateCheck: integer("disable_update_check", { mode: "boolean" }), confirmTabClose: integer("confirm_tab_close", { mode: "boolean" }), hiddenRailTabs: text("hidden_rail_tabs"), + // null means the user has not been asked yet; the assistant stays hidden + // until this is explicitly true and the admin global is on. + aiAssistantEnabled: integer("ai_assistant_enabled", { mode: "boolean" }), + // Opt-in to letting the assistant run allowlisted read-only diagnostics + // without a per-command approval click. + aiReadOnlyCommands: integer("ai_read_only_commands", { mode: "boolean" }), compactHostView: integer("compact_host_view", { mode: "boolean" }), statusColorScheme: text("status_color_scheme"), + customThemes: text("custom_themes"), + customKeybindings: text("custom_keybindings"), + terminalDefaults: text("terminal_defaults"), + rdpDefaults: text("rdp_defaults"), + terminalMacros: text("terminal_macros"), updatedAt: text("updated_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), }); -export const hostMetricsPreferences = sqliteTable("host_metrics_preferences", { +export const hostMetricsPreferences = sqliteTable( + "host_metrics_preferences", + { id: integer("id").primaryKey({ autoIncrement: true }), userId: text("user_id") .notNull() @@ -835,9 +1175,79 @@ export const hostMetricsPreferences = sqliteTable("host_metrics_preferences", { updatedAt: text("updated_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), + }, + // One layout per user per host. Enforced in production since the inline DDL + // creates it, but it was never declared here, so the generated Postgres and + // MySQL schemas lacked it โ€” and the upsert has nothing to conflict on. + (table) => [ + uniqueIndex("idx_host_metrics_prefs_user_host").on(table.userId, table.hostId), + ], +); + +export const proxmoxStatsPreferences = sqliteTable( + "proxmox_stats_preferences", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id").notNull().references(() => hosts.id, { onDelete: "cascade" }), + // JSON-encoded ProxmoxStatsLayout. Layout has no secrets, so it is stored as + // plain JSON (no field-level encryption), same convention as hostMetricsPreferences.layout. + layout: text("layout").notNull(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("idx_proxmox_stats_prefs_user_host").on(table.userId, table.hostId), + ], +); + +export const hostSidebarPreferences = sqliteTable("host_sidebar_preferences", { + userId: text("user_id") + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded HostSidebarPreferences. No secrets in this blob, stored as + // plain JSON like hostMetricsPreferences.layout. + data: text("data").notNull(), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), }); -export const hostHealthChecks = sqliteTable("host_health_checks", { +export const credentialSidebarPreferences = sqliteTable( + "credential_sidebar_preferences", + { + userId: text("user_id") + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded CredentialSidebarPreferences. No secrets in this blob, + // same convention as hostSidebarPreferences.data. + data: text("data").notNull(), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, +); + +export const uiPreferences = sqliteTable("ui_preferences", { + userId: text("user_id") + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + // JSON-encoded UiPreferences (preset + per-area overrides + onboarding + // state). No secrets in this blob, same convention as + // hostSidebarPreferences.data. + data: text("data").notNull(), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const hostHealthChecks = sqliteTable( + "host_health_checks", + { id: integer("id").primaryKey({ autoIncrement: true }), userId: text("user_id") .notNull() @@ -854,7 +1264,12 @@ export const hostHealthChecks = sqliteTable("host_health_checks", { updatedAt: text("updated_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), -}); + }, + // Same as above: one set of checks per user per host. + (table) => [ + uniqueIndex("idx_host_health_checks_user_host").on(table.userId, table.hostId), + ], +); export const hostHealthHistory = sqliteTable("host_health_history", { id: integer("id").primaryKey({ autoIncrement: true }), @@ -879,9 +1294,13 @@ export const dashboardServiceLinks = sqliteTable("dashboard_service_links", { label: text("label").notNull(), url: text("url").notNull(), order: integer("order").notNull().default(0), + syncId: text("sync_id").unique(), createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), }); // --- termix-id begin --- @@ -991,6 +1410,23 @@ export const hostMetricsHistory = sqliteTable("host_metrics_history", { }); // --- metrics-history end --- +// --- proxmox-node-history begin --- +export const proxmoxNodeHistory = sqliteTable("proxmox_node_history", { + id: integer("id").primaryKey({ autoIncrement: true }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + ts: text("ts") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + cpuPercent: real("cpu_percent"), + memPercent: real("mem_percent"), + diskPercent: real("disk_percent"), + netRxBytes: integer("net_rx_bytes"), + netTxBytes: integer("net_tx_bytes"), +}); +// --- proxmox-node-history end --- + // --- alerts begin --- export const alertRules = sqliteTable("alert_rules", { id: integer("id").primaryKey({ autoIncrement: true }), @@ -1036,44 +1472,218 @@ export const alertRuleChannels = sqliteTable("alert_rule_channels", { .references(() => notificationChannels.id, { onDelete: "cascade" }), }); -export const alertFirings = sqliteTable("alert_firings", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - ruleId: integer("rule_id") - .notNull() - .references(() => alertRules.id, { onDelete: "cascade" }), - hostId: integer("host_id").notNull(), - hostName: text("host_name").notNull(), - firedAt: text("fired_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - resolvedAt: text("resolved_at"), - value: real("value"), - message: text("message").notNull(), - severity: text("severity").notNull().default("warning"), - acknowledged: integer("acknowledged", { mode: "boolean" }).notNull().default(false), -}); +export const alertFirings = sqliteTable( + "alert_firings", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + ruleId: integer("rule_id") + .notNull() + .references(() => alertRules.id, { onDelete: "cascade" }), + hostId: integer("host_id").notNull(), + hostName: text("host_name").notNull(), + firedAt: text("fired_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + resolvedAt: text("resolved_at"), + value: real("value"), + message: text("message").notNull(), + severity: text("severity").notNull().default("warning"), + acknowledged: integer("acknowledged", { mode: "boolean" }) + .notNull() + .default(false), + }, + // A rule's history is read newest-first; host_id is filtered on its own. + (table) => [ + index("idx_alert_firings_rule").on(table.ruleId, table.firedAt), + index("idx_alert_firings_host").on(table.hostId), + ], +); // --- alerts end --- +// --- automations begin --- +export const automations = sqliteTable( + "automations", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + description: text("description"), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + // The whole trigger + steps graph, shaped by AutomationDefinition. Read and + // written as a unit, never queried by its inner structure. + definition: text("definition").notNull(), + definitionVersion: integer("definition_version").notNull().default(1), + concurrencyPolicy: text("concurrency_policy").notNull().default("skip"), + maxRunSeconds: integer("max_run_seconds").notNull().default(300), + dryRun: integer("dry_run", { mode: "boolean" }).notNull().default(false), + lastRunAt: text("last_run_at"), + lastRunStatus: text("last_run_status"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // The scheduler sweeps enabled automations for one user at a time. + (table) => [index("idx_automations_user").on(table.userId, table.enabled)], +); + +/** + * Durable per-target trigger state. state_key scopes a trigger to what it is + * actually watching ("", ":/data", ":"), so + * a sustained-breach window can track one filesystem rather than a whole host. + * Living in the database rather than memory means cooldowns and dwell windows + * survive a restart. + */ +export const automationTriggerState = sqliteTable( + "automation_trigger_state", + { + id: integer("id").primaryKey({ autoIncrement: true }), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + stateKey: text("state_key").notNull(), + breachStartedAt: text("breach_started_at"), + lastFiredAt: text("last_fired_at"), + lastValue: real("last_value"), + lastObservedState: text("last_observed_state"), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("idx_automation_trigger_state_key").on( + table.automationId, + table.stateKey, + ), + ], +); + +export const automationSchedules = sqliteTable( + "automation_schedules", + { + id: integer("id").primaryKey({ autoIncrement: true }), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + cron: text("cron"), + intervalSeconds: integer("interval_seconds"), + timezone: text("timezone"), + nextDueAt: text("next_due_at"), + lastTickAt: text("last_tick_at"), + }, + (table) => [ + uniqueIndex("idx_automation_schedules_automation").on(table.automationId), + index("idx_automation_schedules_due").on(table.nextDueAt), + ], +); + +export const automationRuns = sqliteTable( + "automation_runs", + { + id: integer("id").primaryKey({ autoIncrement: true }), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + triggerType: text("trigger_type").notNull(), + triggerContext: text("trigger_context"), + status: text("status").notNull(), + startedAt: text("started_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + finishedAt: text("finished_at"), + durationMs: integer("duration_ms"), + error: text("error"), + dryRun: integer("dry_run", { mode: "boolean" }).notNull().default(false), + // Set when one automation invoked another, so a chain can be traced. + parentRunId: integer("parent_run_id"), + }, + (table) => [ + index("idx_automation_runs_automation").on( + table.automationId, + table.startedAt, + ), + index("idx_automation_runs_user").on(table.userId, table.startedAt), + ], +); + +export const automationRunSteps = sqliteTable( + "automation_run_steps", + { + id: integer("id").primaryKey({ autoIncrement: true }), + runId: integer("run_id") + .notNull() + .references(() => automationRuns.id, { onDelete: "cascade" }), + stepIndex: integer("step_index").notNull(), + stepId: text("step_id").notNull(), + stepType: text("step_type").notNull(), + status: text("status").notNull(), + startedAt: text("started_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + finishedAt: text("finished_at"), + output: text("output"), + error: text("error"), + truncated: integer("truncated", { mode: "boolean" }) + .notNull() + .default(false), + }, + (table) => [ + index("idx_automation_run_steps_run").on(table.runId, table.stepIndex), + ], +); + +export const automationChannels = sqliteTable( + "automation_channels", + { + id: integer("id").primaryKey({ autoIncrement: true }), + automationId: integer("automation_id") + .notNull() + .references(() => automations.id, { onDelete: "cascade" }), + channelId: integer("channel_id") + .notNull() + .references(() => notificationChannels.id, { onDelete: "cascade" }), + }, + (table) => [ + uniqueIndex("idx_automation_channels_pair").on( + table.automationId, + table.channelId, + ), + ], +); +// --- automations end --- + // --- homepage begin --- -export const homepageItems = sqliteTable("homepage_items", { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - typeId: text("type_id").notNull(), - title: text("title"), - config: text("config").notNull().default("{}"), - folderId: integer("folder_id"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), -}); +export const homepageItems = sqliteTable( + "homepage_items", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + typeId: text("type_id").notNull(), + title: text("title"), + config: text("config").notNull().default("{}"), + folderId: integer("folder_id"), + syncId: text("sync_id").unique(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [index("idx_homepage_items_user_id").on(table.userId)], +); export const homepageLayouts = sqliteTable("homepage_layouts", { id: integer("id").primaryKey({ autoIncrement: true }), @@ -1088,3 +1698,241 @@ export const homepageLayouts = sqliteTable("homepage_layouts", { .default(sql`CURRENT_TIMESTAMP`), }); // --- homepage end --- + +// --- fleets begin --- +export const fleets = sqliteTable("fleets", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + description: text("description"), + color: text("color"), + icon: text("icon"), + // JSON array of { tag: string } rules, unioned with static fleetMembers at + // resolution time. Kept to tag-equality matching for v1. + tagRules: text("tag_rules"), + syncId: text("sync_id").unique(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const fleetMembers = sqliteTable( + "fleet_members", + { + id: integer("id").primaryKey({ autoIncrement: true }), + fleetId: integer("fleet_id") + .notNull() + .references(() => fleets.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + addedAt: text("added_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // fleet_id leads the unique pair, so listing a fleet's hosts is already + // served. Finding the fleets a host belongs to starts from host_id. + (table) => [ + uniqueIndex("idx_fleet_members_fleet_host").on(table.fleetId, table.hostId), + index("idx_fleet_members_host").on(table.hostId), + ], +); + +// Latest-only inventory snapshot per host, overwritten on each refresh - no +// historical log, matching the "latest snapshot only" scope decision. +export const fleetInventory = sqliteTable( + "fleet_inventory", + { + id: integer("id").primaryKey({ autoIncrement: true }), + hostId: integer("host_id") + .notNull() + .references(() => hosts.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + osPrettyName: text("os_pretty_name"), + kernel: text("kernel"), + architecture: text("architecture"), + hostname: text("hostname"), + uptimeSeconds: integer("uptime_seconds"), + ip: text("ip"), + packageManager: text("package_manager"), + collectedAt: text("collected_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + // host_id leads the unique pair; a user's whole inventory is read by user_id. + (table) => [ + uniqueIndex("idx_fleet_inventory_host").on(table.hostId, table.userId), + index("idx_fleet_inventory_user").on(table.userId), + ], +); +// --- fleets end --- + +// --- workspaces begin --- +export const userWorkspaces = sqliteTable( + "user_workspaces", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + color: text("color"), + icon: text("icon"), + // "manual" | "last_session" - exactly one last_session row per user. + kind: text("kind").notNull().default("manual"), + isDefault: integer("is_default", { mode: "boolean" }) + .notNull() + .default(false), + // JSON-encoded WorkspacePayload: tabs, splitMode, paneTabIds, rowSizes, rowColSizes + payload: text("payload").notNull().default("{}"), + syncId: text("sync_id").unique(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + lastUsedAt: text("last_used_at"), + }, + (table) => [index("idx_user_workspaces_user_id").on(table.userId)], +); +// --- workspaces end --- + +// --- sync begin --- +// Records a delete for a synced entity type so the other side of a sync +// pair (embedded desktop backend <-> connected remote server) learns about +// the deletion instead of re-creating the row on its next pull. +export const syncTombstones = sqliteTable("sync_tombstones", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + entityType: text("entity_type").notNull(), + syncId: text("sync_id").notNull(), + deletedAt: text("deleted_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); +// --- sync end --- + +// --- ai begin --- +/** + * A user's connection to one AI provider. api_key is encrypted at rest via + * FieldCrypto; it is never returned to the frontend, which only ever sees + * api_key_prefix for display. + */ +export const aiProviders = sqliteTable( + "ai_providers", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // ollama | anthropic | openai | gemini | openai_compatible + providerType: text("provider_type").notNull(), + label: text("label").notNull(), + // Required for ollama and openai_compatible, optional elsewhere. + baseUrl: text("base_url"), + apiKey: text("api_key", { length: 8192 }), + // First few characters, kept in the clear so the UI can identify a key. + apiKeyPrefix: text("api_key_prefix"), + defaultModel: text("default_model"), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + uniqueIndex("idx_ai_providers_user_label").on(table.userId, table.label), + ], +); + +export const aiConversations = sqliteTable( + "ai_conversations", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + title: text("title"), + providerId: integer("provider_id"), + model: text("model"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ai_conversations_user").on(table.userId, table.updatedAt), + ], +); + +export const aiMessages = sqliteTable( + "ai_messages", + { + id: integer("id").primaryKey({ autoIncrement: true }), + conversationId: integer("conversation_id") + .notNull() + .references(() => aiConversations.id, { onDelete: "cascade" }), + // user | assistant | tool + role: text("role").notNull(), + content: text("content").notNull().default(""), + // Serialized tool calls and their results for this turn. + toolCalls: text("tool_calls"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ai_messages_conversation").on( + table.conversationId, + table.createdAt, + ), + ], +); + +/** + * A change the assistant wants to make. Nothing here has been applied: the + * payload is re-validated against the tool schema at apply time and only then + * dispatched through the same repository logic a human action uses. + */ +export const aiProposals = sqliteTable( + "ai_proposals", + { + id: integer("id").primaryKey({ autoIncrement: true }), + conversationId: integer("conversation_id") + .notNull() + .references(() => aiConversations.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + // The propose_* tool name that produced this. + kind: text("kind").notNull(), + summary: text("summary"), + payload: text("payload").notNull().default("{}"), + // pending | applied | rejected | expired + status: text("status").notNull().default("pending"), + appliedAt: text("applied_at"), + resultSummary: text("result_summary"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + index("idx_ai_proposals_user").on(table.userId, table.status), + index("idx_ai_proposals_conversation").on(table.conversationId), + ], +); +// --- ai end --- diff --git a/src/backend/database/repositories/ai-repository.ts b/src/backend/database/repositories/ai-repository.ts new file mode 100644 index 0000000..cf313f7 --- /dev/null +++ b/src/backend/database/repositories/ai-repository.ts @@ -0,0 +1,407 @@ +import { and, desc, eq } from "drizzle-orm"; +import { + aiConversations, + aiMessages, + aiProposals, + aiProviders, +} from "../db/schema.js"; +import { DataCrypto } from "../../utils/data-crypto.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; +import { formatSqlTimestamp } from "./sql-timestamp.js"; + +const now = (): string => formatSqlTimestamp(new Date()); + +export type AiProviderRecord = typeof aiProviders.$inferSelect; +export type AiConversationRecord = typeof aiConversations.$inferSelect; +export type AiMessageRecord = typeof aiMessages.$inferSelect; +export type AiProposalRecord = typeof aiProposals.$inferSelect; + +export interface AiProviderInput { + userId: string; + providerType: string; + label: string; + baseUrl?: string | null; + apiKey?: string | null; + defaultModel?: string | null; + enabled?: boolean; +} + +/** + * Keeps the first few characters so the UI can tell two keys apart without + * ever receiving the key itself. + */ +export function apiKeyPrefix(apiKey: string | null | undefined): string | null { + if (!apiKey) return null; + return apiKey.slice(0, 6); +} + +export class AiRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + /** + * Provider API keys are field-encrypted like any other credential. The key is + * derived from the row id, which does not exist until after the insert, so a + * new provider is written once and then re-encrypted in place with its real + * id -- the same approach AlertRepository uses for channel configs. + */ + private userDataKey(userId: string): Buffer | null { + try { + return DataCrypto.getUserDataKey(userId); + } catch { + // Crypto is not initialized (tests, early boot); leave the value as is. + return null; + } + } + + private encryptApiKey( + apiKey: string, + userId: string, + recordId: number | string, + ): string { + const userDataKey = this.userDataKey(userId); + if (!userDataKey) return apiKey; + return DataCrypto.encryptRecord( + "ai_providers", + { id: recordId, apiKey }, + userId, + userDataKey, + ).apiKey; + } + + private decryptApiKey( + apiKey: string, + userId: string, + recordId: number | string, + ): string { + const userDataKey = this.userDataKey(userId); + if (!userDataKey) return apiKey; + try { + return DataCrypto.decryptRecord( + "ai_providers", + { id: recordId, apiKey }, + userId, + userDataKey, + ).apiKey; + } catch { + // Rows written before encryption was enabled are still plaintext. + return apiKey; + } + } + + // --- providers --- + + /** Never includes the key material; callers get the masked prefix only. */ + async listProviders(userId: string): Promise { + const rows = await this.context.drizzle + .select() + .from(aiProviders) + .where(eq(aiProviders.userId, userId)) + .orderBy(aiProviders.id); + + return rows.map((row) => ({ ...row, apiKey: null })); + } + + async findProvider( + id: number, + userId: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(aiProviders) + .where(and(eq(aiProviders.id, id), eq(aiProviders.userId, userId))) + .limit(1); + + if (!rows[0]) return null; + return { ...rows[0], apiKey: null }; + } + + /** + * The one path that returns usable key material. Only the provider adapters + * call this, immediately before an outbound request. + */ + async findProviderWithSecret( + id: number, + userId: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(aiProviders) + .where(and(eq(aiProviders.id, id), eq(aiProviders.userId, userId))) + .limit(1); + + const row = rows[0]; + if (!row) return null; + if (!row.apiKey) return row; + return { + ...row, + apiKey: this.decryptApiKey(row.apiKey, userId, row.id), + }; + } + + async createProvider(input: AiProviderInput): Promise { + const [created] = await insertReturning(this.context, aiProviders, { + userId: input.userId, + providerType: input.providerType, + label: input.label, + baseUrl: input.baseUrl ?? null, + apiKey: input.apiKey ?? null, + apiKeyPrefix: apiKeyPrefix(input.apiKey), + defaultModel: input.defaultModel ?? null, + enabled: input.enabled ?? true, + }); + + if (input.apiKey) { + const encrypted = this.encryptApiKey( + input.apiKey, + input.userId, + created.id, + ); + if (encrypted !== input.apiKey) { + await this.context.drizzle + .update(aiProviders) + .set({ apiKey: encrypted }) + .where(eq(aiProviders.id, created.id)); + } + } + + await this.afterWrite(); + return { ...created, apiKey: null }; + } + + async updateProvider( + id: number, + userId: string, + input: Partial>, + ): Promise { + const existing = await this.findProvider(id, userId); + if (!existing) return null; + + const updates: Record = { updatedAt: now() }; + if (input.providerType !== undefined) + updates.providerType = input.providerType; + if (input.label !== undefined) updates.label = input.label; + if (input.baseUrl !== undefined) updates.baseUrl = input.baseUrl; + if (input.defaultModel !== undefined) + updates.defaultModel = input.defaultModel; + if (input.enabled !== undefined) updates.enabled = input.enabled; + + // An empty string clears the key; undefined leaves it untouched. + if (input.apiKey !== undefined) { + if (input.apiKey) { + updates.apiKey = this.encryptApiKey(input.apiKey, userId, id); + updates.apiKeyPrefix = apiKeyPrefix(input.apiKey); + } else { + updates.apiKey = null; + updates.apiKeyPrefix = null; + } + } + + await this.context.drizzle + .update(aiProviders) + .set(updates) + .where(and(eq(aiProviders.id, id), eq(aiProviders.userId, userId))); + + await this.afterWrite(); + return this.findProvider(id, userId); + } + + async deleteProvider(id: number, userId: string): Promise { + const result = await this.context.drizzle + .delete(aiProviders) + .where(and(eq(aiProviders.id, id), eq(aiProviders.userId, userId))); + + const deleted = rowsAffected(result) > 0; + if (deleted) await this.afterWrite(); + return deleted; + } + + // --- conversations --- + + async listConversations( + userId: string, + limit = 50, + ): Promise { + return this.context.drizzle + .select() + .from(aiConversations) + .where(eq(aiConversations.userId, userId)) + .orderBy(desc(aiConversations.updatedAt)) + .limit(limit); + } + + async findConversation( + id: number, + userId: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(aiConversations) + .where( + and(eq(aiConversations.id, id), eq(aiConversations.userId, userId)), + ) + .limit(1); + return rows[0] ?? null; + } + + async createConversation(input: { + userId: string; + title?: string | null; + providerId?: number | null; + model?: string | null; + }): Promise { + const [created] = await insertReturning(this.context, aiConversations, { + userId: input.userId, + title: input.title ?? null, + providerId: input.providerId ?? null, + model: input.model ?? null, + }); + await this.afterWrite(); + return created; + } + + async touchConversation(id: number, title?: string | null): Promise { + const updates: Record = { updatedAt: now() }; + if (title) updates.title = title; + await this.context.drizzle + .update(aiConversations) + .set(updates) + .where(eq(aiConversations.id, id)); + await this.afterWrite(); + } + + async deleteConversation(id: number, userId: string): Promise { + const result = await this.context.drizzle + .delete(aiConversations) + .where( + and(eq(aiConversations.id, id), eq(aiConversations.userId, userId)), + ); + const deleted = rowsAffected(result) > 0; + if (deleted) await this.afterWrite(); + return deleted; + } + + // --- messages --- + + async listMessages(conversationId: number): Promise { + return this.context.drizzle + .select() + .from(aiMessages) + .where(eq(aiMessages.conversationId, conversationId)) + .orderBy(aiMessages.id); + } + + async appendMessage(input: { + conversationId: number; + role: string; + content: string; + toolCalls?: string | null; + }): Promise { + const [created] = await insertReturning(this.context, aiMessages, { + conversationId: input.conversationId, + role: input.role, + content: input.content, + toolCalls: input.toolCalls ?? null, + }); + await this.afterWrite(); + return created; + } + + // --- proposals --- + + async listProposals( + userId: string, + conversationId?: number, + ): Promise { + const where = conversationId + ? and( + eq(aiProposals.userId, userId), + eq(aiProposals.conversationId, conversationId), + ) + : eq(aiProposals.userId, userId); + + return this.context.drizzle + .select() + .from(aiProposals) + .where(where) + .orderBy(desc(aiProposals.id)); + } + + async findProposal( + id: number, + userId: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(aiProposals) + .where(and(eq(aiProposals.id, id), eq(aiProposals.userId, userId))) + .limit(1); + return rows[0] ?? null; + } + + async createProposal(input: { + conversationId: number; + userId: string; + kind: string; + summary?: string | null; + payload: string; + }): Promise { + const [created] = await insertReturning(this.context, aiProposals, { + conversationId: input.conversationId, + userId: input.userId, + kind: input.kind, + summary: input.summary ?? null, + payload: input.payload, + status: "pending", + }); + await this.afterWrite(); + return created; + } + + async setProposalStatus( + id: number, + userId: string, + status: "applied" | "rejected" | "expired", + resultSummary?: string | null, + ): Promise { + const result = await this.context.drizzle + .update(aiProposals) + .set({ + status, + appliedAt: status === "applied" ? now() : null, + resultSummary: resultSummary ?? null, + }) + .where( + and( + eq(aiProposals.id, id), + eq(aiProposals.userId, userId), + eq(aiProposals.status, "pending"), + ), + ); + + const updated = rowsAffected(result) > 0; + if (updated) await this.afterWrite(); + return updated; + } + + // --- account deletion --- + + async deleteByUserId(userId: string): Promise { + // ai_messages and ai_proposals cascade from ai_conversations. + await this.context.drizzle + .delete(aiConversations) + .where(eq(aiConversations.userId, userId)); + await this.context.drizzle + .delete(aiProviders) + .where(eq(aiProviders.userId, userId)); + await this.afterWrite(); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/alert-repository.ts b/src/backend/database/repositories/alert-repository.ts index 9b55ca3..f3f845e 100644 --- a/src/backend/database/repositories/alert-repository.ts +++ b/src/backend/database/repositories/alert-repository.ts @@ -1,4 +1,4 @@ -import { and, count, desc, eq, inArray, isNull, or } from "drizzle-orm"; +import { and, count, desc, eq, inArray, isNull, lt, or } from "drizzle-orm"; import { alertFirings, alertRuleChannels, @@ -6,7 +6,11 @@ import { hosts, notificationChannels, } from "../db/schema.js"; +import { DataCrypto } from "../../utils/data-crypto.js"; import type { DatabaseContext } from "./database-context.js"; +import { sqlTimestampDaysAgo } from "./sql-timestamp.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; type AlertRuleRecord = typeof alertRules.$inferSelect; type NotificationChannelRecord = typeof notificationChannels.$inferSelect; @@ -80,6 +84,56 @@ export class AlertRepository { private readonly onWrite?: () => void | Promise, ) {} + /** + * Channel configs carry ntfy tokens and webhook auth headers, so they are + * field-encrypted like any other secret. The key is derived from the row id, + * which does not exist until after the insert, so a new channel is written + * once and then re-encrypted in place with its real id. + */ + private userDataKey(userId: string): Buffer | null { + try { + return DataCrypto.getUserDataKey(userId); + } catch { + // Crypto is not initialized (tests, early boot); leave the value as is. + return null; + } + } + + private encryptConfig( + config: string, + userId: string, + recordId: number | string, + ): string { + const userDataKey = this.userDataKey(userId); + if (!userDataKey) return config; + return DataCrypto.encryptRecord( + "notification_channels", + { id: recordId, config }, + userId, + userDataKey, + ).config; + } + + private decryptConfig( + config: string, + userId: string, + recordId: number | string, + ): string { + const userDataKey = this.userDataKey(userId); + if (!userDataKey) return config; + try { + return DataCrypto.decryptRecord( + "notification_channels", + { id: recordId, config }, + userId, + userDataKey, + ).config; + } catch { + // Rows written before channel configs were encrypted are still plaintext. + return config; + } + } + async listNotificationChannels( userId: string, ): Promise { @@ -89,7 +143,11 @@ export class AlertRepository { .where(eq(notificationChannels.userId, userId)) .orderBy(notificationChannels.id); - return rows.map(mapChannelRow); + return rows.map((row) => { + const mapped = mapChannelRow(row); + mapped.config = this.decryptConfig(mapped.config, userId, mapped.id); + return mapped; + }); } async findNotificationChannelForUser( @@ -107,7 +165,10 @@ export class AlertRepository { ) .limit(1); - return rows[0] ? mapChannelRow(rows[0]) : null; + if (!rows[0]) return null; + const mapped = mapChannelRow(rows[0]); + mapped.config = this.decryptConfig(mapped.config, userId, mapped.id); + return mapped; } async createNotificationChannel(input: { @@ -117,19 +178,34 @@ export class AlertRepository { config: string; enabled: boolean; }): Promise { - const [created] = await this.context.drizzle - .insert(notificationChannels) - .values({ + const [created] = await insertReturning( + this.context, + notificationChannels, + { userId: input.userId, name: input.name, type: input.type, config: input.config, enabled: input.enabled, - }) - .returning(); + }, + ); + + const encrypted = this.encryptConfig( + input.config, + input.userId, + created.id, + ); + if (encrypted !== input.config) { + await this.context.drizzle + .update(notificationChannels) + .set({ config: encrypted }) + .where(eq(notificationChannels.id, created.id)); + } await this.afterWrite(); - return mapChannelRow(created); + const mapped = mapChannelRow(created); + mapped.config = input.config; + return mapped; } async updateNotificationChannel( @@ -146,37 +222,42 @@ export class AlertRepository { return this.findNotificationChannelForUser(id, userId); } - const [updated] = await this.context.drizzle - .update(notificationChannels) - .set(input) - .where( - and( - eq(notificationChannels.id, id), - eq(notificationChannels.userId, userId), - ), - ) - .returning(); + const values = + input.config !== undefined + ? { ...input, config: this.encryptConfig(input.config, userId, id) } + : input; + + const [updated] = await updateReturning( + this.context, + notificationChannels, + values, + and( + eq(notificationChannels.id, id), + eq(notificationChannels.userId, userId), + ), + ); if (!updated) return null; await this.afterWrite(); - return mapChannelRow(updated); + const mapped = mapChannelRow(updated); + mapped.config = this.decryptConfig(mapped.config, userId, mapped.id); + return mapped; } async deleteNotificationChannel( id: number, userId: string, ): Promise { - const deleted = await this.context.drizzle + const result = await this.context.drizzle .delete(notificationChannels) .where( and( eq(notificationChannels.id, id), eq(notificationChannels.userId, userId), ), - ) - .returning({ id: notificationChannels.id }); + ); - if (deleted.length === 0) return false; + if (rowsAffected(result) === 0) return false; await this.afterWrite(); return true; } @@ -210,21 +291,18 @@ export class AlertRepository { channels: number[]; now: string; }): Promise { - const [created] = await this.context.drizzle - .insert(alertRules) - .values({ - userId: input.userId, - hostId: input.hostId, - name: input.name, - enabled: input.enabled, - triggerType: input.triggerType, - thresholdValue: input.thresholdValue, - thresholdDurationSeconds: input.thresholdDurationSeconds, - cooldownMinutes: input.cooldownMinutes, - createdAt: input.now, - updatedAt: input.now, - }) - .returning(); + const [created] = await insertReturning(this.context, alertRules, { + userId: input.userId, + hostId: input.hostId, + name: input.name, + enabled: input.enabled, + triggerType: input.triggerType, + thresholdValue: input.thresholdValue, + thresholdDurationSeconds: input.thresholdDurationSeconds, + cooldownMinutes: input.cooldownMinutes, + createdAt: input.now, + updatedAt: input.now, + }); const channels = await this.replaceRuleChannels( created.id, @@ -263,9 +341,10 @@ export class AlertRepository { now: string; }, ): Promise { - const [updated] = await this.context.drizzle - .update(alertRules) - .set({ + const [updated] = await updateReturning( + this.context, + alertRules, + { ...(input.name !== undefined ? { name: input.name } : {}), ...(input.hostId !== undefined ? { hostId: input.hostId } : {}), ...(input.enabled !== undefined ? { enabled: input.enabled } : {}), @@ -282,9 +361,9 @@ export class AlertRepository { ? { cooldownMinutes: input.cooldownMinutes } : {}), updatedAt: input.now, - }) - .where(and(eq(alertRules.id, id), eq(alertRules.userId, userId))) - .returning(); + }, + and(eq(alertRules.id, id), eq(alertRules.userId, userId)), + ); if (!updated) return null; @@ -298,12 +377,11 @@ export class AlertRepository { } async deleteAlertRule(id: number, userId: string): Promise { - const deleted = await this.context.drizzle + const result = await this.context.drizzle .delete(alertRules) - .where(and(eq(alertRules.id, id), eq(alertRules.userId, userId))) - .returning({ id: alertRules.id }); + .where(and(eq(alertRules.id, id), eq(alertRules.userId, userId))); - if (deleted.length === 0) return false; + if (rowsAffected(result) === 0) return false; await this.afterWrite(); return true; } @@ -359,17 +437,25 @@ export class AlertRepository { await this.afterWrite(); } + // A rule with host_id IS NULL means "all my hosts", not "all hosts on the + // server". Without joining the host back to its owner, every user's wildcard + // rule fired for every polled host and leaked other users' host names into + // their alerts. async listEnabledRulesForHost(hostId: number): Promise { const rows = await this.context.drizzle - .select() + .select({ rule: alertRules }) .from(alertRules) + .innerJoin(hosts, eq(hosts.id, hostId)) .where( and( eq(alertRules.enabled, true), - or(eq(alertRules.hostId, hostId), isNull(alertRules.hostId)), + or( + eq(alertRules.hostId, hostId), + and(isNull(alertRules.hostId), eq(alertRules.userId, hosts.userId)), + ), ), ); - return rows.map(mapEngineRule); + return rows.map((row) => mapEngineRule(row.rule)); } async listEnabledRulesForHostUser( @@ -411,12 +497,15 @@ export class AlertRepository { await this.afterWrite(); } - pruneFiringsOlderThan(userId: string, days: number): void { - this.context.sqlite - ?.prepare( - "DELETE FROM alert_firings WHERE user_id = ? AND fired_at < datetime('now', ?)", - ) - .run(userId, `-${days} days`); + async pruneFiringsOlderThan(userId: string, days: number): Promise { + await this.context.drizzle + .delete(alertFirings) + .where( + and( + eq(alertFirings.userId, userId), + lt(alertFirings.firedAt, sqlTimestampDaysAgo(days)), + ), + ); } async deleteByUserId(userId: string): Promise<{ @@ -438,10 +527,9 @@ export class AlertRepository { .where(eq(notificationChannels.userId, userId)) ).map((row) => row.id); - const firingRows = await this.context.drizzle + const firingResult = await this.context.drizzle .delete(alertFirings) - .where(eq(alertFirings.userId, userId)) - .returning({ id: alertFirings.id }); + .where(eq(alertFirings.userId, userId)); const linkFilters = [ ...(ruleIds.length > 0 @@ -451,37 +539,34 @@ export class AlertRepository { ? [inArray(alertRuleChannels.channelId, channelIds)] : []), ]; - const linkRows = + const linkResult = linkFilters.length === 0 - ? [] + ? null : await this.context.drizzle .delete(alertRuleChannels) - .where(or(...linkFilters)) - .returning({ id: alertRuleChannels.id }); + .where(or(...linkFilters)); - const ruleRows = await this.context.drizzle + const ruleResult = await this.context.drizzle .delete(alertRules) - .where(eq(alertRules.userId, userId)) - .returning({ id: alertRules.id }); - const channelRows = await this.context.drizzle + .where(eq(alertRules.userId, userId)); + const result = await this.context.drizzle .delete(notificationChannels) - .where(eq(notificationChannels.userId, userId)) - .returning({ id: notificationChannels.id }); + .where(eq(notificationChannels.userId, userId)); if ( - firingRows.length > 0 || - linkRows.length > 0 || - ruleRows.length > 0 || - channelRows.length > 0 + rowsAffected(firingResult) > 0 || + rowsAffected(linkResult) > 0 || + rowsAffected(ruleResult) > 0 || + rowsAffected(result) > 0 ) { await this.afterWrite(); } return { - firingsDeleted: firingRows.length, - ruleLinksDeleted: linkRows.length, - rulesDeleted: ruleRows.length, - channelsDeleted: channelRows.length, + firingsDeleted: rowsAffected(firingResult), + ruleLinksDeleted: rowsAffected(linkResult), + rulesDeleted: rowsAffected(ruleResult), + channelsDeleted: rowsAffected(result), }; } @@ -491,6 +576,7 @@ export class AlertRepository { const rows = await this.context.drizzle .select({ id: notificationChannels.id, + userId: notificationChannels.userId, type: notificationChannels.type, config: notificationChannels.config, enabled: notificationChannels.enabled, @@ -507,7 +593,11 @@ export class AlertRepository { ), ); - return rows; + // The engine sends without a user in scope, so decrypt against the owner. + return rows.map(({ userId, ...channel }) => ({ + ...channel, + config: this.decryptConfig(channel.config, userId, channel.id), + })); } async getHostDisplayName(hostId: number): Promise { diff --git a/src/backend/database/repositories/api-key-repository.ts b/src/backend/database/repositories/api-key-repository.ts index a24b880..c037f78 100644 --- a/src/backend/database/repositories/api-key-repository.ts +++ b/src/backend/database/repositories/api-key-repository.ts @@ -1,6 +1,8 @@ import { eq, and } from "drizzle-orm"; import { apiKeys, users } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { deleteReturning, insertReturning } from "./returning.js"; export type ApiKeyRecord = typeof apiKeys.$inferSelect; export type NewApiKeyRecord = typeof apiKeys.$inferInsert; @@ -24,10 +26,7 @@ export class ApiKeyRepository { ) {} async create(apiKey: NewApiKeyRecord): Promise { - const rows = await this.context.drizzle - .insert(apiKeys) - .values(apiKey) - .returning(); + const rows = await insertReturning(this.context, apiKeys, apiKey); await this.afterWrite(); return rows[0]; } @@ -78,23 +77,23 @@ export class ApiKeyRepository { } async delete(id: string): Promise { - const rows = await this.context.drizzle - .delete(apiKeys) - .where(eq(apiKeys.id, id)) - .returning(); + const rows = await deleteReturning( + this.context, + apiKeys, + eq(apiKeys.id, id), + ); await this.afterWrite(); return rows[0] ?? null; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(apiKeys) - .where(eq(apiKeys.userId, userId)) - .returning({ id: apiKeys.id }); + .where(eq(apiKeys.userId, userId)); await this.afterWrite(); - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/audit-log-repository.ts b/src/backend/database/repositories/audit-log-repository.ts index 6e9c86a..1b01e6c 100644 --- a/src/backend/database/repositories/audit-log-repository.ts +++ b/src/backend/database/repositories/audit-log-repository.ts @@ -1,6 +1,9 @@ -import { and, asc, desc, eq, gte, inArray, lte, sql } from "drizzle-orm"; +import { and, asc, desc, eq, gte, inArray, lt, lte, sql } from "drizzle-orm"; import { auditLogs } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { sqlTimestampDaysAgo } from "./sql-timestamp.js"; +import { databaseLogger } from "../../utils/logger.js"; +import { countValue, rowsAffected } from "./mutation-result.js"; export type AuditLogRecord = typeof auditLogs.$inferSelect; export type NewAuditLogRecord = typeof auditLogs.$inferInsert; @@ -19,10 +22,46 @@ export type AuditLogPage = { total: number; }; -const PRUNE_MAX = 10000; -const PRUNE_TARGET = 9000; +export const AUDIT_RETENTION_DAYS_ENV = "AUDIT_LOG_RETENTION_DAYS"; +export const AUDIT_MAX_ENTRIES_ENV = "AUDIT_LOG_MAX_ENTRIES"; + +const DEFAULT_MAX_ENTRIES = 10000; +const PRUNE_TARGET_RATIO = 0.9; + +function positiveIntEnv(key: string, env: NodeJS.ProcessEnv): number | null { + const raw = Number(env[key]); + return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : null; +} + +/** + * How long entries are kept. Unset means "no time limit", in which case only + * the row cap applies. + */ +export function auditRetentionDays( + env: NodeJS.ProcessEnv = process.env, +): number | null { + return positiveIntEnv(AUDIT_RETENTION_DAYS_ENV, env); +} + +/** Hard ceiling on stored entries, so a busy install cannot fill the disk. */ +export function auditMaxEntries(env: NodeJS.ProcessEnv = process.env): number { + return positiveIntEnv(AUDIT_MAX_ENTRIES_ENV, env) ?? DEFAULT_MAX_ENTRIES; +} export class AuditLogRepository { + /** + * Cached row count backing the cap check. Static because the factory builds + * a repository per call, so a per-instance count would never survive to be + * reused. Null means "unknown, re-read" โ€” which is also how any path that + * deletes rows invalidates it. + */ + private static cachedCount: number | null = null; + + /** Drops the cached count so a test starts from a known state. */ + static resetPruneThrottleForTests(): void { + AuditLogRepository.cachedCount = null; + } + constructor( private readonly context: DatabaseContext, private readonly onWrite?: () => void | Promise, @@ -30,7 +69,7 @@ export class AuditLogRepository { async create(entry: NewAuditLogRecord): Promise { await this.context.drizzle.insert(auditLogs).values(entry); - await this.pruneIfNeeded(); + await this.pruneIfDue(); await this.afterWrite(); } @@ -57,10 +96,31 @@ export class AuditLogRepository { return { logs, - total: totalResult[0]?.count ?? 0, + total: countValue(totalResult[0]?.count), }; } + /** + * Reads matching entries in ascending time order for export. + * + * Paged rather than fetched whole so an export cannot pull an unbounded + * result set into memory, and ascending so a resumed or appended export + * continues where the previous one stopped. + */ + async listForExport(input: { + filters: AuditLogFilters; + limit: number; + offset: number; + }): Promise { + return this.context.drizzle + .select() + .from(auditLogs) + .where(this.buildWhere(input.filters)) + .orderBy(asc(auditLogs.timestamp), asc(auditLogs.id)) + .limit(input.limit) + .offset(input.offset); + } + async listDistinctActions(): Promise { const rows = await this.context.drizzle .selectDistinct({ action: auditLogs.action }) @@ -70,17 +130,40 @@ export class AuditLogRepository { return rows.map((row) => row.action); } - async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle - .delete(auditLogs) - .where(eq(auditLogs.userId, userId)) - .returning({ id: auditLogs.id }); + /** + * Detaches entries from a user being deleted instead of removing them. + * + * The schema already relaxed this foreign key to ON DELETE SET NULL, but the + * account-deletion path deletes the rows explicitly, which undoes that. An + * audit trail that vanishes with the account it recorded cannot answer the + * question it exists for, and offboarding is exactly when that question gets + * asked. `username` is denormalised, so the entry stays attributable. + */ + async anonymizeByUserId(userId: string): Promise { + const result = await this.context.drizzle + .update(auditLogs) + .set({ userId: null }) + .where(eq(auditLogs.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); + } + + async deleteByUserId(userId: string): Promise { + // Row count changed outside the insert path; force a re-read. + AuditLogRepository.cachedCount = null; + const result = await this.context.drizzle + .delete(auditLogs) + .where(eq(auditLogs.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + + return rowsAffected(result); } private buildWhere(filters: AuditLogFilters) { @@ -104,29 +187,138 @@ export class AuditLogRepository { return conditions.length > 0 ? and(...conditions) : undefined; } - private async pruneIfNeeded(): Promise { + /** + * Keeps the two prune passes off the per-write hot path without letting the + * row cap go unenforced. + * + * Both passes used to run inline on every insert: a retention DELETE plus a + * COUNT over the whole table, thousands of times an hour on a busy install, + * almost always to find nothing to do โ€” and audit writes sit in the request + * path of the actions they record. + * + * They are split by what they cost and what they guarantee. Retention is + * time-based, so nothing is lost by checking it on an interval. The row cap + * is a disk-space guard that has to react to inserts, so it is still checked + * on the write that crosses it โ€” but against a cached count, so the common + * case is an integer compare rather than a COUNT. + */ + private async pruneIfDue(): Promise { + try { + // Retention only costs anything on installs that configure it, and the + // DELETE is driven by idx_audit_logs_timestamp, so it stays on the write + // path where its "nothing older than N days survives" guarantee holds. + await this.pruneExpired(); + await this.pruneOverflowIfOverCap(); + } catch (error) { + // Pruning is maintenance; never fail the write that triggered it. + databaseLogger.warn("Audit log prune failed", { + operation: "audit_prune_failed", + error: error instanceof Error ? error.message : String(error), + }); + } + } + + /** + * Enforces the cap using a cached row count, so a steady stream of writes + * costs one COUNT to prime and then nothing until the cap is next reached. + */ + private async pruneOverflowIfOverCap(): Promise { + const max = auditMaxEntries(); + + if (AuditLogRepository.cachedCount === null) { + AuditLogRepository.cachedCount = await this.countAll(); + } else { + AuditLogRepository.cachedCount += 1; + } + + if (AuditLogRepository.cachedCount < max) return; + + // At the cap: re-read for real, since the cached value can drift if rows + // were deleted by another path (user deletion, manual cleanup). + AuditLogRepository.cachedCount = await this.countAll(); + if (AuditLogRepository.cachedCount < max) return; + + await this.pruneOverflow(); + AuditLogRepository.cachedCount = await this.countAll(); + } + + private async countAll(): Promise { + const result = await this.context.drizzle + .select({ count: sql`COUNT(*)` }) + .from(auditLogs); + return countValue(result[0]?.count); + } + + /** Runs the prune regardless of the interval. Exposed for tests and startup. */ + async pruneNow(): Promise { + await this.pruneExpired(); + await this.pruneOverflow(); + AuditLogRepository.cachedCount = null; + } + + /** Drops entries past the configured retention window. */ + private async pruneExpired(): Promise { + const days = auditRetentionDays(); + if (days === null) return; + + const cutoff = sqlTimestampDaysAgo(days); + const result = await this.context.drizzle + .delete(auditLogs) + .where(lt(auditLogs.timestamp, cutoff)); + + if (rowsAffected(result) > 0) { + databaseLogger.info( + `Pruned ${rowsAffected(result)} audit entries past retention`, + { + operation: "audit_retention_prune", + removed: rowsAffected(result), + retentionDays: days, + cutoff, + }, + ); + } + } + + /** + * Enforces the row cap. Unlike retention this discards entries that are still + * within the window, so it is reported as a warning: it means the ceiling is + * too low for how much this install audits, and evidence is being lost. + */ + private async pruneOverflow(): Promise { + const max = auditMaxEntries(); const countResult = await this.context.drizzle .select({ count: sql`COUNT(*)` }) .from(auditLogs); - const count = countResult[0]?.count ?? 0; + const count = countValue(countResult[0]?.count); - if (count < PRUNE_MAX) { - return; - } + if (count < max) return; - const deleteCount = count - PRUNE_TARGET; + const deleteCount = count - Math.floor(max * PRUNE_TARGET_RATIO); const rows = await this.context.drizzle - .select({ id: auditLogs.id }) + .select({ id: auditLogs.id, timestamp: auditLogs.timestamp }) .from(auditLogs) .orderBy(asc(auditLogs.timestamp)) .limit(deleteCount); - const ids = rows.map((row) => row.id); + if (rows.length === 0) return; - if (ids.length > 0) { - await this.context.drizzle - .delete(auditLogs) - .where(inArray(auditLogs.id, ids)); - } + await this.context.drizzle.delete(auditLogs).where( + inArray( + auditLogs.id, + rows.map((row) => row.id), + ), + ); + + databaseLogger.warn( + `Audit log hit its ${max}-entry cap; discarded ${rows.length} entries`, + { + operation: "audit_overflow_prune", + removed: rows.length, + maxEntries: max, + oldestRemoved: rows[0]?.timestamp, + newestRemoved: rows[rows.length - 1]?.timestamp, + hint: `Raise ${AUDIT_MAX_ENTRIES_ENV}, or set ${AUDIT_RETENTION_DAYS_ENV} and export older entries before they are dropped.`, + }, + ); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/automation-repository.ts b/src/backend/database/repositories/automation-repository.ts new file mode 100644 index 0000000..d967f40 --- /dev/null +++ b/src/backend/database/repositories/automation-repository.ts @@ -0,0 +1,784 @@ +import { and, asc, desc, eq, inArray, lt, lte, sql } from "drizzle-orm"; +import { + automationChannels, + automationRunSteps, + automationRuns, + automationSchedules, + automationTriggerState, + automations, + notificationChannels, +} from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; + +type AutomationRecord = typeof automations.$inferSelect; +type AutomationRunRecord = typeof automationRuns.$inferSelect; +type AutomationRunStepRecord = typeof automationRunSteps.$inferSelect; +type TriggerStateRecord = typeof automationTriggerState.$inferSelect; +type ScheduleRecord = typeof automationSchedules.$inferSelect; + +export interface AutomationRow { + id: number; + user_id: string; + name: string; + description: string | null; + enabled: number; + definition: string; + definition_version: number; + concurrency_policy: string; + max_run_seconds: number; + dry_run: number; + last_run_at: string | null; + last_run_status: string | null; + created_at: string; + updated_at: string; + channels: number[]; +} + +export interface AutomationRunRow { + id: number; + automation_id: number; + user_id: string; + trigger_type: string; + trigger_context: string | null; + status: string; + started_at: string; + finished_at: string | null; + duration_ms: number | null; + error: string | null; + dry_run: number; + parent_run_id: number | null; + automation_name?: string | null; +} + +export interface AutomationRunStepRow { + id: number; + run_id: number; + step_index: number; + step_id: string; + step_type: string; + status: string; + started_at: string; + finished_at: string | null; + output: string | null; + error: string | null; + truncated: number; +} + +/** The shape the engine loads; camelCase and already parsed where useful. */ +export interface AutomationEngineRow { + id: number; + userId: string; + name: string; + enabled: boolean; + definition: string; + concurrencyPolicy: string; + maxRunSeconds: number; + dryRun: boolean; +} + +export interface TriggerStateRow { + automationId: number; + stateKey: string; + breachStartedAt: string | null; + lastFiredAt: string | null; + lastValue: number | null; + lastObservedState: string | null; +} + +export interface DueScheduleRow { + automationId: number; + cron: string | null; + intervalSeconds: number | null; + timezone: string | null; + nextDueAt: string | null; +} + +export class AutomationRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async list(userId: string): Promise { + const rows = await this.context.drizzle + .select() + .from(automations) + .where(eq(automations.userId, userId)) + .orderBy(asc(automations.name)); + + if (rows.length === 0) return []; + + // One query for every automation's channels rather than one per row. + const links = await this.context.drizzle + .select({ + automationId: automationChannels.automationId, + channelId: automationChannels.channelId, + }) + .from(automationChannels) + .where( + inArray( + automationChannels.automationId, + rows.map((row) => row.id), + ), + ); + + const byAutomation = new Map(); + for (const link of links) { + const list = byAutomation.get(link.automationId) ?? []; + list.push(link.channelId); + byAutomation.set(link.automationId, list); + } + + return rows.map((row) => mapAutomationRow(row, byAutomation.get(row.id))); + } + + async findForUser(id: number, userId: string): Promise { + const rows = await this.context.drizzle + .select() + .from(automations) + .where(and(eq(automations.id, id), eq(automations.userId, userId))) + .limit(1); + + if (!rows[0]) return null; + return mapAutomationRow(rows[0], await this.listChannelIds(id)); + } + + async findById(id: number): Promise { + const rows = await this.context.drizzle + .select() + .from(automations) + .where(eq(automations.id, id)) + .limit(1); + return rows[0] ? mapEngineRow(rows[0]) : null; + } + + async create(input: { + userId: string; + name: string; + description?: string | null; + enabled?: boolean; + definition: string; + concurrencyPolicy?: string; + maxRunSeconds?: number; + dryRun?: boolean; + channels?: number[]; + now?: string; + }): Promise { + const now = input.now ?? new Date().toISOString(); + const [created] = await insertReturning(this.context, automations, { + userId: input.userId, + name: input.name, + description: input.description ?? null, + enabled: input.enabled ?? true, + definition: input.definition, + concurrencyPolicy: input.concurrencyPolicy ?? "skip", + maxRunSeconds: input.maxRunSeconds ?? 300, + dryRun: input.dryRun ?? false, + createdAt: now, + updatedAt: now, + }); + + const channels = await this.replaceChannels( + created.id, + input.userId, + input.channels ?? [], + ); + await this.afterWrite(); + return mapAutomationRow(created, channels); + } + + async update( + id: number, + userId: string, + input: { + name?: string; + description?: string | null; + enabled?: boolean; + definition?: string; + concurrencyPolicy?: string; + maxRunSeconds?: number; + dryRun?: boolean; + channels?: number[]; + now?: string; + }, + ): Promise { + const { channels, now, ...fields } = input; + const values: Record = { ...fields }; + + if (Object.keys(values).length > 0) { + values.updatedAt = now ?? new Date().toISOString(); + const [updated] = await updateReturning( + this.context, + automations, + values, + and(eq(automations.id, id), eq(automations.userId, userId)), + ); + if (!updated) return null; + } else { + const existing = await this.findForUser(id, userId); + if (!existing) return null; + } + + if (channels) { + await this.replaceChannels(id, userId, channels); + } + + await this.afterWrite(); + return this.findForUser(id, userId); + } + + async delete(id: number, userId: string): Promise { + const result = await this.context.drizzle + .delete(automations) + .where(and(eq(automations.id, id), eq(automations.userId, userId))); + + const deleted = rowsAffected(result) > 0; + if (deleted) await this.afterWrite(); + return deleted; + } + + async setEnabled( + id: number, + userId: string, + enabled: boolean, + ): Promise { + const result = await this.context.drizzle + .update(automations) + .set({ enabled, updatedAt: new Date().toISOString() }) + .where(and(eq(automations.id, id), eq(automations.userId, userId))); + + const changed = rowsAffected(result) > 0; + if (changed) await this.afterWrite(); + return changed; + } + + /** + * Enabled automations owned by whoever owns the given host. Wildcard targets + * must never reach across users, which is the bug the alert engine shipped + * with. + */ + async listEnabledForUser(userId: string): Promise { + const rows = await this.context.drizzle + .select() + .from(automations) + .where( + and(eq(automations.enabled, true), eq(automations.userId, userId)), + ); + return rows.map(mapEngineRow); + } + + async listAllEnabled(): Promise { + const rows = await this.context.drizzle + .select() + .from(automations) + .where(eq(automations.enabled, true)); + return rows.map(mapEngineRow); + } + + // --- trigger state --- + + async getTriggerState( + automationId: number, + stateKey: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(automationTriggerState) + .where( + and( + eq(automationTriggerState.automationId, automationId), + eq(automationTriggerState.stateKey, stateKey), + ), + ) + .limit(1); + return rows[0] ? mapTriggerStateRow(rows[0]) : null; + } + + async upsertTriggerState(input: { + automationId: number; + stateKey: string; + breachStartedAt?: string | null; + lastFiredAt?: string | null; + lastValue?: number | null; + lastObservedState?: string | null; + }): Promise { + const existing = await this.getTriggerState( + input.automationId, + input.stateKey, + ); + const updatedAt = new Date().toISOString(); + + if (existing) { + const values: Record = { updatedAt }; + if (input.breachStartedAt !== undefined) + values.breachStartedAt = input.breachStartedAt; + if (input.lastFiredAt !== undefined) + values.lastFiredAt = input.lastFiredAt; + if (input.lastValue !== undefined) values.lastValue = input.lastValue; + if (input.lastObservedState !== undefined) + values.lastObservedState = input.lastObservedState; + + await this.context.drizzle + .update(automationTriggerState) + .set(values) + .where( + and( + eq(automationTriggerState.automationId, input.automationId), + eq(automationTriggerState.stateKey, input.stateKey), + ), + ); + } else { + await this.context.drizzle.insert(automationTriggerState).values({ + automationId: input.automationId, + stateKey: input.stateKey, + breachStartedAt: input.breachStartedAt ?? null, + lastFiredAt: input.lastFiredAt ?? null, + lastValue: input.lastValue ?? null, + lastObservedState: input.lastObservedState ?? null, + updatedAt, + }); + } + await this.afterWrite(); + } + + async clearBreach(automationId: number, stateKey: string): Promise { + await this.context.drizzle + .update(automationTriggerState) + .set({ breachStartedAt: null, updatedAt: new Date().toISOString() }) + .where( + and( + eq(automationTriggerState.automationId, automationId), + eq(automationTriggerState.stateKey, stateKey), + ), + ); + await this.afterWrite(); + } + + /** Dwell windows the scheduler has to re-check without a fresh sample. */ + async listOpenBreaches(): Promise { + const rows = await this.context.drizzle + .select() + .from(automationTriggerState) + .where(sql`${automationTriggerState.breachStartedAt} IS NOT NULL`); + return rows.map(mapTriggerStateRow); + } + + // --- schedules --- + + async upsertSchedule(input: { + automationId: number; + cron: string | null; + intervalSeconds: number | null; + timezone: string | null; + nextDueAt: string | null; + }): Promise { + const existing = await this.context.drizzle + .select({ id: automationSchedules.id }) + .from(automationSchedules) + .where(eq(automationSchedules.automationId, input.automationId)) + .limit(1); + + if (existing[0]) { + await this.context.drizzle + .update(automationSchedules) + .set({ + cron: input.cron, + intervalSeconds: input.intervalSeconds, + timezone: input.timezone, + nextDueAt: input.nextDueAt, + }) + .where(eq(automationSchedules.automationId, input.automationId)); + } else { + await this.context.drizzle.insert(automationSchedules).values(input); + } + await this.afterWrite(); + } + + async deleteSchedule(automationId: number): Promise { + await this.context.drizzle + .delete(automationSchedules) + .where(eq(automationSchedules.automationId, automationId)); + await this.afterWrite(); + } + + /** Schedules due at or before `now`, joined to their enabled automation. */ + async listDueSchedules(now: string): Promise { + const rows = await this.context.drizzle + .select({ + automationId: automationSchedules.automationId, + cron: automationSchedules.cron, + intervalSeconds: automationSchedules.intervalSeconds, + timezone: automationSchedules.timezone, + nextDueAt: automationSchedules.nextDueAt, + }) + .from(automationSchedules) + .innerJoin( + automations, + eq(automations.id, automationSchedules.automationId), + ) + .where( + and( + eq(automations.enabled, true), + lte(automationSchedules.nextDueAt, now), + ), + ); + return rows; + } + + async markScheduleTicked( + automationId: number, + nextDueAt: string | null, + lastTickAt: string, + ): Promise { + await this.context.drizzle + .update(automationSchedules) + .set({ nextDueAt, lastTickAt }) + .where(eq(automationSchedules.automationId, automationId)); + await this.afterWrite(); + } + + // --- runs --- + + async createRun(input: { + automationId: number; + userId: string; + triggerType: string; + triggerContext?: string | null; + status: string; + dryRun?: boolean; + parentRunId?: number | null; + now?: string; + }): Promise { + const [created] = await insertReturning(this.context, automationRuns, { + automationId: input.automationId, + userId: input.userId, + triggerType: input.triggerType, + triggerContext: input.triggerContext ?? null, + status: input.status, + startedAt: input.now ?? new Date().toISOString(), + dryRun: input.dryRun ?? false, + parentRunId: input.parentRunId ?? null, + }); + await this.afterWrite(); + return mapRunRow(created); + } + + async finishRun( + runId: number, + input: { + status: string; + error?: string | null; + finishedAt?: string; + durationMs?: number | null; + }, + ): Promise { + const finishedAt = input.finishedAt ?? new Date().toISOString(); + await this.context.drizzle + .update(automationRuns) + .set({ + status: input.status, + error: input.error ?? null, + finishedAt, + durationMs: input.durationMs ?? null, + }) + .where(eq(automationRuns.id, runId)); + + const run = await this.context.drizzle + .select({ + automationId: automationRuns.automationId, + startedAt: automationRuns.startedAt, + }) + .from(automationRuns) + .where(eq(automationRuns.id, runId)) + .limit(1); + + if (run[0]) { + await this.context.drizzle + .update(automations) + .set({ lastRunAt: run[0].startedAt, lastRunStatus: input.status }) + .where(eq(automations.id, run[0].automationId)); + } + await this.afterWrite(); + } + + async listRuns( + userId: string, + options: { automationId?: number; limit?: number; offset?: number } = {}, + ): Promise { + const limit = Math.min(Math.max(options.limit ?? 50, 1), 200); + const offset = Math.max(options.offset ?? 0, 0); + + const where = options.automationId + ? and( + eq(automationRuns.userId, userId), + eq(automationRuns.automationId, options.automationId), + ) + : eq(automationRuns.userId, userId); + + const rows = await this.context.drizzle + .select({ + run: automationRuns, + automationName: automations.name, + }) + .from(automationRuns) + .leftJoin(automations, eq(automations.id, automationRuns.automationId)) + .where(where) + .orderBy(desc(automationRuns.startedAt), desc(automationRuns.id)) + .limit(limit) + .offset(offset); + + return rows.map((row) => ({ + ...mapRunRow(row.run), + automation_name: row.automationName ?? null, + })); + } + + async findRunForUser( + runId: number, + userId: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(automationRuns) + .where( + and(eq(automationRuns.id, runId), eq(automationRuns.userId, userId)), + ) + .limit(1); + return rows[0] ? mapRunRow(rows[0]) : null; + } + + async countRunningFor(automationId: number): Promise { + const rows = await this.context.drizzle + .select({ id: automationRuns.id }) + .from(automationRuns) + .where( + and( + eq(automationRuns.automationId, automationId), + eq(automationRuns.status, "running"), + ), + ); + return rows.length; + } + + // --- run steps --- + + async createRunStep(input: { + runId: number; + stepIndex: number; + stepId: string; + stepType: string; + status: string; + now?: string; + }): Promise { + const [created] = await insertReturning(this.context, automationRunSteps, { + runId: input.runId, + stepIndex: input.stepIndex, + stepId: input.stepId, + stepType: input.stepType, + status: input.status, + startedAt: input.now ?? new Date().toISOString(), + }); + await this.afterWrite(); + return created.id; + } + + async finishRunStep( + stepRowId: number, + input: { + status: string; + output?: string | null; + error?: string | null; + truncated?: boolean; + finishedAt?: string; + }, + ): Promise { + await this.context.drizzle + .update(automationRunSteps) + .set({ + status: input.status, + output: input.output ?? null, + error: input.error ?? null, + truncated: input.truncated ?? false, + finishedAt: input.finishedAt ?? new Date().toISOString(), + }) + .where(eq(automationRunSteps.id, stepRowId)); + await this.afterWrite(); + } + + async listRunSteps(runId: number): Promise { + const rows = await this.context.drizzle + .select() + .from(automationRunSteps) + .where(eq(automationRunSteps.runId, runId)) + .orderBy(asc(automationRunSteps.stepIndex)); + return rows.map(mapRunStepRow); + } + + /** + * Trims run history. Called from the scheduler's daily sweep rather than on + * every write, which is what made the alert engine's pruning expensive. + */ + async pruneRunsOlderThan(days: number): Promise { + const cutoff = new Date(Date.now() - days * 86400000).toISOString(); + const result = await this.context.drizzle + .delete(automationRuns) + .where(lt(automationRuns.startedAt, cutoff)); + const deleted = rowsAffected(result); + if (deleted > 0) await this.afterWrite(); + return deleted; + } + + /** Marks runs left behind by a crash so they do not block concurrency. */ + async failStaleRunningRuns(olderThanIso: string): Promise { + const result = await this.context.drizzle + .update(automationRuns) + .set({ + status: "failed", + error: "Interrupted by a server restart", + finishedAt: new Date().toISOString(), + }) + .where( + and( + eq(automationRuns.status, "running"), + lt(automationRuns.startedAt, olderThanIso), + ), + ); + const affected = rowsAffected(result); + if (affected > 0) await this.afterWrite(); + return affected; + } + + async deleteByUserId(userId: string): Promise { + const result = await this.context.drizzle + .delete(automations) + .where(eq(automations.userId, userId)); + const deleted = rowsAffected(result); + if (deleted > 0) await this.afterWrite(); + return deleted; + } + + private async listChannelIds(automationId: number): Promise { + const rows = await this.context.drizzle + .select({ channelId: automationChannels.channelId }) + .from(automationChannels) + .where(eq(automationChannels.automationId, automationId)); + return rows.map((row) => row.channelId); + } + + private async replaceChannels( + automationId: number, + userId: string, + channelIds: number[], + ): Promise { + await this.context.drizzle + .delete(automationChannels) + .where(eq(automationChannels.automationId, automationId)); + + if (channelIds.length === 0) return []; + + // One lookup for the whole set instead of a query per channel. + const owned = await this.context.drizzle + .select({ id: notificationChannels.id }) + .from(notificationChannels) + .where( + and( + eq(notificationChannels.userId, userId), + inArray(notificationChannels.id, channelIds), + ), + ); + + const linked = owned.map((row) => row.id); + if (linked.length > 0) { + await this.context.drizzle + .insert(automationChannels) + .values(linked.map((channelId) => ({ automationId, channelId }))); + } + return linked; + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} + +function mapAutomationRow( + row: AutomationRecord, + channels: number[] = [], +): AutomationRow { + return { + id: row.id, + user_id: row.userId, + name: row.name, + description: row.description ?? null, + enabled: row.enabled ? 1 : 0, + definition: row.definition, + definition_version: row.definitionVersion, + concurrency_policy: row.concurrencyPolicy, + max_run_seconds: row.maxRunSeconds, + dry_run: row.dryRun ? 1 : 0, + last_run_at: row.lastRunAt ?? null, + last_run_status: row.lastRunStatus ?? null, + created_at: row.createdAt, + updated_at: row.updatedAt, + channels, + }; +} + +function mapEngineRow(row: AutomationRecord): AutomationEngineRow { + return { + id: row.id, + userId: row.userId, + name: row.name, + enabled: !!row.enabled, + definition: row.definition, + concurrencyPolicy: row.concurrencyPolicy, + maxRunSeconds: row.maxRunSeconds, + dryRun: !!row.dryRun, + }; +} + +function mapTriggerStateRow(row: TriggerStateRecord): TriggerStateRow { + return { + automationId: row.automationId, + stateKey: row.stateKey, + breachStartedAt: row.breachStartedAt ?? null, + lastFiredAt: row.lastFiredAt ?? null, + lastValue: row.lastValue ?? null, + lastObservedState: row.lastObservedState ?? null, + }; +} + +function mapRunRow(row: AutomationRunRecord): AutomationRunRow { + return { + id: row.id, + automation_id: row.automationId, + user_id: row.userId, + trigger_type: row.triggerType, + trigger_context: row.triggerContext ?? null, + status: row.status, + started_at: row.startedAt, + finished_at: row.finishedAt ?? null, + duration_ms: row.durationMs ?? null, + error: row.error ?? null, + dry_run: row.dryRun ? 1 : 0, + parent_run_id: row.parentRunId ?? null, + }; +} + +function mapRunStepRow(row: AutomationRunStepRecord): AutomationRunStepRow { + return { + id: row.id, + run_id: row.runId, + step_index: row.stepIndex, + step_id: row.stepId, + step_type: row.stepType, + status: row.status, + started_at: row.startedAt, + finished_at: row.finishedAt ?? null, + output: row.output ?? null, + error: row.error ?? null, + truncated: row.truncated ? 1 : 0, + }; +} diff --git a/src/backend/database/repositories/c2s-tunnel-preset-repository.ts b/src/backend/database/repositories/c2s-tunnel-preset-repository.ts index b041713..0c14689 100644 --- a/src/backend/database/repositories/c2s-tunnel-preset-repository.ts +++ b/src/backend/database/repositories/c2s-tunnel-preset-repository.ts @@ -1,6 +1,8 @@ import { and, asc, eq, sql } from "drizzle-orm"; import { c2sTunnelPresets } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; export type C2sTunnelPresetRecord = typeof c2sTunnelPresets.$inferSelect; @@ -64,16 +66,13 @@ export class C2sTunnelPresetRepository { userId: string, input: C2sTunnelPresetCreateInput, ): Promise { - const [created] = await this.context.drizzle - .insert(c2sTunnelPresets) - .values({ - userId, - name: input.name, - config: input.config, - platform: input.platform ?? null, - computerName: input.computerName ?? null, - }) - .returning(); + const [created] = await insertReturning(this.context, c2sTunnelPresets, { + userId, + name: input.name, + config: input.config, + platform: input.platform ?? null, + computerName: input.computerName ?? null, + }); await this.afterWrite(); return created; @@ -84,16 +83,15 @@ export class C2sTunnelPresetRepository { id: number, updates: C2sTunnelPresetUpdateInput, ): Promise { - const [updated] = await this.context.drizzle - .update(c2sTunnelPresets) - .set({ + const [updated] = await updateReturning( + this.context, + c2sTunnelPresets, + { ...updates, updatedAt: sql`CURRENT_TIMESTAMP`, - }) - .where( - and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)), - ) - .returning(); + }, + and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)), + ); if (updated) { await this.afterWrite(); @@ -103,31 +101,29 @@ export class C2sTunnelPresetRepository { } async deleteForUser(userId: string, id: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(c2sTunnelPresets) .where( and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)), - ) - .returning({ id: c2sTunnelPresets.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(c2sTunnelPresets) - .where(eq(c2sTunnelPresets.userId, userId)) - .returning({ id: c2sTunnelPresets.id }); + .where(eq(c2sTunnelPresets.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/command-history-repository.ts b/src/backend/database/repositories/command-history-repository.ts index 5bf72b3..da7b657 100644 --- a/src/backend/database/repositories/command-history-repository.ts +++ b/src/backend/database/repositories/command-history-repository.ts @@ -1,6 +1,8 @@ import { and, desc, eq, inArray, sql } from "drizzle-orm"; import { commandHistory } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; export type CommandHistoryRecord = typeof commandHistory.$inferSelect; @@ -16,10 +18,12 @@ export class CommandHistoryRepository { command: string, executedAt = new Date().toISOString(), ): Promise { - const [created] = await this.context.drizzle - .insert(commandHistory) - .values({ userId, hostId, command, executedAt }) - .returning(); + const [created] = await insertReturning(this.context, commandHistory, { + userId, + hostId, + command, + executedAt, + }); await this.afterWrite(); return created; } @@ -76,7 +80,7 @@ export class CommandHistoryRepository { hostId: number, command: string, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(commandHistory) .where( and( @@ -84,45 +88,42 @@ export class CommandHistoryRepository { eq(commandHistory.hostId, hostId), eq(commandHistory.command, command), ), - ) - .returning({ id: commandHistory.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByUserAndHost(userId: string, hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(commandHistory) .where( and( eq(commandHistory.userId, userId), eq(commandHistory.hostId, hostId), ), - ) - .returning({ id: commandHistory.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(commandHistory) - .where(eq(commandHistory.hostId, hostId)) - .returning({ id: commandHistory.id }); + .where(eq(commandHistory.hostId, hostId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostIds(hostIds: number[]): Promise { @@ -130,29 +131,27 @@ export class CommandHistoryRepository { return 0; } - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(commandHistory) - .where(inArray(commandHistory.hostId, hostIds)) - .returning({ id: commandHistory.id }); + .where(inArray(commandHistory.hostId, hostIds)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(commandHistory) - .where(eq(commandHistory.userId, userId)) - .returning({ id: commandHistory.id }); + .where(eq(commandHistory.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/credential-repository.ts b/src/backend/database/repositories/credential-repository.ts index 82d0af3..b6d3c2e 100644 --- a/src/backend/database/repositories/credential-repository.ts +++ b/src/backend/database/repositories/credential-repository.ts @@ -1,7 +1,14 @@ import { and, desc, eq, sql } from "drizzle-orm"; +import { randomUUID } from "crypto"; import { sshCredentials, sshCredentialUsage } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; export type CredentialRecord = typeof sshCredentials.$inferSelect; export type NewCredentialRecord = typeof sshCredentials.$inferInsert; @@ -16,10 +23,10 @@ export class CredentialRepository { ) {} async create(credential: NewCredentialRecord): Promise { - const rows = await this.context.drizzle - .insert(sshCredentials) - .values(credential) - .returning(); + const rows = await insertReturning(this.context, sshCredentials, { + syncId: randomUUID(), + ...credential, + }); await this.afterWrite(); return rows[0]; } @@ -30,7 +37,11 @@ export class CredentialRepository { ): Promise { const userDataKey = DataCrypto.validateUserAccess(userId); const tempId = credential.id ?? Date.now(); - const dataWithTempId = { ...credential, id: tempId }; + const dataWithTempId = { + syncId: randomUUID(), + ...credential, + id: tempId, + }; const encryptedCredential = this.encryptCredentialRecordForWrite( dataWithTempId, userId, @@ -41,10 +52,11 @@ export class CredentialRepository { delete (encryptedCredential as Partial).id; } - const rows = await this.context.drizzle - .insert(sshCredentials) - .values(encryptedCredential as NewCredentialRecord) - .returning(); + const rows = await insertReturning( + this.context, + sshCredentials, + encryptedCredential as NewCredentialRecord, + ); await this.afterWrite(); return DataCrypto.decryptRecord( @@ -138,22 +150,21 @@ export class CredentialRepository { oldName: string, newName: string, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(sshCredentials) - .set({ folder: newName }) + .set({ folder: newName, updatedAt: sql`CURRENT_TIMESTAMP` }) .where( and( eq(sshCredentials.userId, userId), eq(sshCredentials.folder, oldName), ), - ) - .returning({ id: sshCredentials.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async updateForUser( @@ -161,16 +172,15 @@ export class CredentialRepository { credentialId: number, update: CredentialUpdate, ): Promise { - const rows = await this.context.drizzle - .update(sshCredentials) - .set(update) - .where( - and( - eq(sshCredentials.id, credentialId), - eq(sshCredentials.userId, userId), - ), - ) - .returning(); + const rows = await updateReturning( + this.context, + sshCredentials, + { ...update, updatedAt: sql`CURRENT_TIMESTAMP` }, + and( + eq(sshCredentials.id, credentialId), + eq(sshCredentials.userId, userId), + ), + ); await this.afterWrite(); return rows[0] ?? null; @@ -188,47 +198,97 @@ export class CredentialRepository { userDataKey, ); - const rows = await this.context.drizzle - .update(sshCredentials) - .set(encryptedUpdate) - .where( - and( - eq(sshCredentials.id, credentialId), - eq(sshCredentials.userId, userId), - ), - ) - .returning(); + const rows = await updateReturning( + this.context, + sshCredentials, + { ...encryptedUpdate, updatedAt: sql`CURRENT_TIMESTAMP` }, + and( + eq(sshCredentials.id, credentialId), + eq(sshCredentials.userId, userId), + ), + ); await this.afterWrite(); return this.decryptOne(rows[0] ?? null, userId); } - async deleteForUser(userId: string, credentialId: number): Promise { - const rows = await this.context.drizzle - .delete(sshCredentials) - .where( - and( - eq(sshCredentials.id, credentialId), - eq(sshCredentials.userId, userId), - ), - ) - .returning({ id: sshCredentials.id }); + /** + * Manual drag-to-reorder write path. Updates sortOrder for each id one row + * at a time inside a transaction rather than a single set-for-all-matching + * -ids statement, matching HostRepository.reorderForUser. + */ + async reorderForUser( + userId: string, + positions: { id: number; sortOrder: number }[], + ): Promise { + if (positions.length === 0) return 0; - await this.afterWrite(); - return rows.length > 0; - } + let affected: number; + if (this.context.dialect === "sqlite") { + affected = this.context.drizzle.transaction((tx) => { + let count = 0; + for (const { id, sortOrder } of positions) { + const result = tx + .update(sshCredentials) + .set({ sortOrder, updatedAt: sql`CURRENT_TIMESTAMP` }) + .where( + and(eq(sshCredentials.id, id), eq(sshCredentials.userId, userId)), + ) + .run(); + count += rowsAffected(result); + } + return count; + }); + } else { + affected = await this.context.drizzle.transaction(async (tx) => { + let count = 0; + for (const { id, sortOrder } of positions) { + const result = await tx + .update(sshCredentials) + .set({ sortOrder, updatedAt: sql`CURRENT_TIMESTAMP` }) + .where( + and(eq(sshCredentials.id, id), eq(sshCredentials.userId, userId)), + ); + count += rowsAffected(result); + } + return count; + }); + } - async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle - .delete(sshCredentials) - .where(eq(sshCredentials.userId, userId)) - .returning({ id: sshCredentials.id }); - - if (rows.length > 0) { + if (affected > 0) { await this.afterWrite(); } - return rows.length; + return affected; + } + + async deleteForUser( + userId: string, + credentialId: number, + ): Promise<{ syncId: string | null } | null> { + const rows = await deleteReturning( + this.context, + sshCredentials, + and( + eq(sshCredentials.id, credentialId), + eq(sshCredentials.userId, userId), + ), + ); + + await this.afterWrite(); + return rows[0] ? { syncId: rows[0].syncId } : null; + } + + async deleteByUserId(userId: string): Promise { + const result = await this.context.drizzle + .delete(sshCredentials) + .where(eq(sshCredentials.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + + return rowsAffected(result); } async recordUsage( diff --git a/src/backend/database/repositories/credential-sidebar-preference-repository.ts b/src/backend/database/repositories/credential-sidebar-preference-repository.ts new file mode 100644 index 0000000..fabe6ab --- /dev/null +++ b/src/backend/database/repositories/credential-sidebar-preference-repository.ts @@ -0,0 +1,71 @@ +import { eq } from "drizzle-orm"; +import { credentialSidebarPreferences } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturningWhere, updateReturning } from "./returning.js"; + +export type CredentialSidebarPreferenceRecord = + typeof credentialSidebarPreferences.$inferSelect; + +export class CredentialSidebarPreferenceRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async findByUserId( + userId: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(credentialSidebarPreferences) + .where(eq(credentialSidebarPreferences.userId, userId)) + .limit(1); + + return rows[0] ?? null; + } + + async upsert( + userId: string, + data: string, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findByUserId(userId); + + if (!existing) { + const rows = await insertReturningWhere( + this.context, + credentialSidebarPreferences, + { userId, data, updatedAt: now }, + eq(credentialSidebarPreferences.userId, userId), + ); + await this.afterWrite(); + return rows[0]; + } + + const rows = await updateReturning( + this.context, + credentialSidebarPreferences, + { data, updatedAt: now }, + eq(credentialSidebarPreferences.userId, userId), + ); + await this.afterWrite(); + return rows[0]; + } + + async deleteByUserId(userId: string): Promise { + const result = await this.context.drizzle + .delete(credentialSidebarPreferences) + .where(eq(credentialSidebarPreferences.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + + return rowsAffected(result); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/dashboard-service-link-repository.ts b/src/backend/database/repositories/dashboard-service-link-repository.ts index a12079e..3aa6d25 100644 --- a/src/backend/database/repositories/dashboard-service-link-repository.ts +++ b/src/backend/database/repositories/dashboard-service-link-repository.ts @@ -1,6 +1,13 @@ import { and, asc, eq } from "drizzle-orm"; +import { randomUUID } from "crypto"; import { dashboardServiceLinks } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; export type DashboardServiceLinkRecord = typeof dashboardServiceLinks.$inferSelect; @@ -37,16 +44,19 @@ export class DashboardServiceLinkRepository { const nextOrder = existing.length > 0 ? existing[existing.length - 1].order + 1 : 0; - const [created] = await this.context.drizzle - .insert(dashboardServiceLinks) - .values({ + const [created] = await insertReturning( + this.context, + dashboardServiceLinks, + { + syncId: randomUUID(), userId, label: input.label, url: input.url, order: nextOrder, createdAt, - }) - .returning(); + updatedAt: createdAt, + }, + ); await this.afterWrite(); return created; } @@ -74,16 +84,15 @@ export class DashboardServiceLinkRepository { id: number, updates: DashboardServiceLinkUpdate, ): Promise { - const [updated] = await this.context.drizzle - .update(dashboardServiceLinks) - .set(updates) - .where( - and( - eq(dashboardServiceLinks.id, id), - eq(dashboardServiceLinks.userId, userId), - ), - ) - .returning(); + const [updated] = await updateReturning( + this.context, + dashboardServiceLinks, + { ...updates, updatedAt: new Date().toISOString() }, + and( + eq(dashboardServiceLinks.id, id), + eq(dashboardServiceLinks.userId, userId), + ), + ); if (updated) { await this.afterWrite(); @@ -92,35 +101,34 @@ export class DashboardServiceLinkRepository { return updated ?? null; } - async deleteForUser(userId: string, id: number): Promise { - const rows = await this.context.drizzle - .delete(dashboardServiceLinks) - .where( - and( - eq(dashboardServiceLinks.id, id), - eq(dashboardServiceLinks.userId, userId), - ), - ) - .returning({ id: dashboardServiceLinks.id }); + async deleteForUser( + userId: string, + id: number, + ): Promise<{ syncId: string | null } | null> { + const rows = await deleteReturning( + this.context, + dashboardServiceLinks, + and( + eq(dashboardServiceLinks.id, id), + eq(dashboardServiceLinks.userId, userId), + ), + ); - if (rows.length > 0) { - await this.afterWrite(); - } - - return rows.length > 0; + if (rows.length === 0) return null; + await this.afterWrite(); + return { syncId: rows[0].syncId }; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(dashboardServiceLinks) - .where(eq(dashboardServiceLinks.userId, userId)) - .returning({ id: dashboardServiceLinks.id }); + .where(eq(dashboardServiceLinks.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/database-context.ts b/src/backend/database/repositories/database-context.ts index 7666cc4..4f0bb47 100644 --- a/src/backend/database/repositories/database-context.ts +++ b/src/backend/database/repositories/database-context.ts @@ -1,9 +1,41 @@ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; -import type { Database as BetterSqliteDatabase } from "better-sqlite3"; import type * as schema from "../db/schema.js"; +// Re-exported so repositories can keep importing it from here, but defined in +// db/dialect.ts โ€” a local copy that said "sqlite" survived here for a while and +// typed every context as SQLite-only while the runtime already carried all +// three, which silently made the dialect branches unreachable to the checker. +export type { DatabaseDialect } from "../db/dialect.js"; +import type { DatabaseDialect } from "../db/dialect.js"; + +/** + * The database handle repositories work against. + * + * Typed as the SQLite instance on purpose. drizzle's three Database classes + * share no base class and their signatures are incompatible: a union is not + * callable, and a generic would have to be threaded through all 43 + * repositories and every method on them. + * + * This is a deliberate approximation, not an accident. The query-builder + * surface the repositories actually use is the same on all three engines, and + * that equivalence is asserted in multi-dialect.test.ts rather than assumed โ€” + * identifier quoting, placeholder style and value coercion are all covered + * there. At runtime this may hold a Postgres or MySQL instance. + * + * The one place the surfaces genuinely differ is RETURNING, which MySQL lacks; + * see mutation-result.ts for how that is absorbed. + */ +export type PortableDatabase = BetterSQLite3Database; + +/** + * What a repository is allowed to touch. + * + * Deliberately drizzle-only: with no raw driver handle here, no repository can + * reach for engine-specific SQL. Retention queries that previously needed + * `datetime('now', ?)` compute their cutoff in JS instead โ€” see + * ./sql-timestamp.ts. + */ export interface DatabaseContext { - dialect: "sqlite"; - drizzle: BetterSQLite3Database; - sqlite?: BetterSqliteDatabase; + dialect: DatabaseDialect; + drizzle: PortableDatabase; } diff --git a/src/backend/database/repositories/dismissed-alert-repository.ts b/src/backend/database/repositories/dismissed-alert-repository.ts index e44d20b..87cf40d 100644 --- a/src/backend/database/repositories/dismissed-alert-repository.ts +++ b/src/backend/database/repositories/dismissed-alert-repository.ts @@ -1,6 +1,7 @@ import { and, eq } from "drizzle-orm"; import { dismissedAlerts } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; export type DismissedAlertRecord = typeof dismissedAlerts.$inferSelect; @@ -72,34 +73,32 @@ export class DismissedAlertRepository { } async deleteForUser(userId: string, alertId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(dismissedAlerts) .where( and( eq(dismissedAlerts.userId, userId), eq(dismissedAlerts.alertId, alertId), ), - ) - .returning({ id: dismissedAlerts.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(dismissedAlerts) - .where(eq(dismissedAlerts.userId, userId)) - .returning({ id: dismissedAlerts.id }); + .where(eq(dismissedAlerts.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts index 0db7fea..5cd2679 100644 --- a/src/backend/database/repositories/factory.ts +++ b/src/backend/database/repositories/factory.ts @@ -1,8 +1,12 @@ import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; import { getDb, getSqlite } from "../db/index.js"; +import { needsExplicitPersist, resolveDatabaseDialect } from "../db/dialect.js"; +import { primeSettingsCache, readCachedSetting } from "./settings-cache.js"; import type { DatabaseContext } from "./database-context.js"; import { WebauthnCredentialRepository } from "./webauthn-credential-repository.js"; +import { AiRepository } from "./ai-repository.js"; import { AlertRepository } from "./alert-repository.js"; +import { AutomationRepository } from "./automation-repository.js"; import { ApiKeyRepository } from "./api-key-repository.js"; import { AuditLogRepository } from "./audit-log-repository.js"; import { C2sTunnelPresetRepository } from "./c2s-tunnel-preset-repository.js"; @@ -11,14 +15,20 @@ import { CredentialRepository } from "./credential-repository.js"; import { DashboardServiceLinkRepository } from "./dashboard-service-link-repository.js"; import { DismissedAlertRepository } from "./dismissed-alert-repository.js"; import { FileManagerBookmarkRepository } from "./file-manager-bookmark-repository.js"; +import { FleetRepository } from "./fleet-repository.js"; +import { FleetInventoryRepository } from "./fleet-inventory-repository.js"; import { HomepageItemRepository } from "./homepage-item-repository.js"; import { HomepageLayoutRepository } from "./homepage-layout-repository.js"; import { HostFolderRepository } from "./host-folder-repository.js"; import { HostHealthRepository } from "./host-health-repository.js"; import { HostMetricsHistoryRepository } from "./host-metrics-history-repository.js"; import { HostMetricsPreferenceRepository } from "./host-metrics-preference-repository.js"; +import { ProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js"; import { HostRepository } from "./host-repository.js"; import { HostResolutionRepository } from "./host-resolution-repository.js"; +import { HostSidebarPreferenceRepository } from "./host-sidebar-preference-repository.js"; +import { CredentialSidebarPreferenceRepository } from "./credential-sidebar-preference-repository.js"; +import { UiPreferenceRepository } from "./ui-preference-repository.js"; import { NetworkTopologyRepository } from "./network-topology-repository.js"; import { OpenTabRepository } from "./open-tab-repository.js"; import { OpksshTokenRepository } from "./opkssh-token-repository.js"; @@ -27,10 +37,13 @@ import { RecentActivityRepository } from "./recent-activity-repository.js"; import { RoleRepository } from "./role-repository.js"; import { SessionRecordingRepository } from "./session-recording-repository.js"; import { SessionRepository } from "./session-repository.js"; +import { SessionShareRepository } from "./session-share-repository.js"; import { SettingsRepository } from "./settings-repository.js"; +import { SharedHostAuthOverrideRepository } from "./shared-host-auth-override-repository.js"; import { SharedHostSecretsRepository } from "./shared-host-secrets-repository.js"; import { SnippetRepository } from "./snippet-repository.js"; import { SshCredentialUsageRepository } from "./ssh-credential-usage-repository.js"; +import { SyncTombstoneRepository } from "./sync-tombstone-repository.js"; import { SsoProviderRepository } from "./sso-provider-repository.js"; import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js"; import { TermixIdentityRepository } from "./termix-identity-repository.js"; @@ -42,26 +55,81 @@ import { UserPreferenceRepository } from "./user-preference-repository.js"; import { UserRepository } from "./user-repository.js"; import { VaultProfileRepository } from "./vault-profile-repository.js"; import { VaultTokenRepository } from "./vault-token-repository.js"; +import { WorkspaceRepository } from "./workspace-repository.js"; +/** + * The context every repository runs against. + * + * The dialect has to be resolved, not assumed: it is what `returning.ts` reads + * to decide whether it can ask for RETURNING, and whether an upsert spells + * itself `onConflictDoUpdate` or `onDuplicateKeyUpdate`. Reporting "sqlite" + * while connected to MySQL makes the second of those a TypeError on the first + * write. + * + * Both cross-dialect harnesses build a DatabaseContext themselves, so neither + * exercises this function โ€” see tests/database/repositories/factory-context. + */ export function createCurrentRepositoryContext(): DatabaseContext { return { - dialect: "sqlite", + dialect: resolveDatabaseDialect(), drizzle: getDb(), - sqlite: getSqlite(), }; } +/** + * Post-write hook handed to every repository. + * + * Only meaningful for SQLite, where the database lives in memory and has to be + * serialised back to its encrypted file. On Postgres and MySQL the write is + * already durable, so no hook is installed at all rather than one that does + * nothing โ€” repositories call it as `this.onWrite?.()`. + */ export function createCurrentRepositoryWriteHook( reason: string, -): () => Promise { +): (() => Promise) | undefined { + if (!needsExplicitPersist(resolveDatabaseDialect())) return undefined; return () => DatabaseSaveTrigger.forceSave(reason); } +/** + * Post-write hook for high-frequency, non-critical writes (telemetry + * inserts/cleanup, informational timestamp touches). + * + * Marks the in-memory database dirty and lets DatabaseSaveTrigger's existing + * debounce coalesce the actual serialize+encrypt, instead of forcing one on + * every single sample. A lost 2-second window of telemetry on crash is + * acceptable; blocking the event loop that serves SSH traffic on every metric + * sample is not. + */ +export function createCurrentRepositoryLazyWriteHook( + reason: string, +): (() => Promise) | undefined { + if (!needsExplicitPersist(resolveDatabaseDialect())) return undefined; + return () => DatabaseSaveTrigger.triggerSave(reason); +} + +/** + * Raw driver handle for the few synchronous call sites that cannot await โ€” + * getCurrentSettingValue below, and settings reads during startup. Repositories + * must not use this: they take a DatabaseContext, which is drizzle-only. + * Porting to another engine means giving these callers an async path first. + */ export function getCurrentRepositorySqlite() { return getSqlite(); } +/** + * Synchronous settings read. + * + * SQLite can be queried synchronously, so it is read directly and stays + * authoritative. Other engines have no synchronous query, so the value comes + * from the cache primed at startup and kept current by SettingsRepository. + */ export function getCurrentSettingValue(key: string): string | null { + if (!needsExplicitPersist(resolveDatabaseDialect())) { + return readCachedSetting(key); + } + const row = getCurrentRepositorySqlite() .prepare("SELECT value FROM settings WHERE key = ?") .get(key) as { value?: string } | undefined; @@ -76,6 +144,13 @@ export function createCurrentWebauthnCredentialRepository(): WebauthnCredentialR ); } +export function createCurrentAiRepository(): AiRepository { + return new AiRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("ai_repository_write"), + ); +} + export function createCurrentAlertRepository(): AlertRepository { return new AlertRepository( createCurrentRepositoryContext(), @@ -83,6 +158,13 @@ export function createCurrentAlertRepository(): AlertRepository { ); } +export function createCurrentAutomationRepository(): AutomationRepository { + return new AutomationRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("automation_repository_write"), + ); +} + export function createCurrentApiKeyRepository(): ApiKeyRepository { return new ApiKeyRepository( createCurrentRepositoryContext(), @@ -125,6 +207,13 @@ export function createCurrentDashboardServiceLinkRepository(): DashboardServiceL ); } +export function createCurrentSyncTombstoneRepository(): SyncTombstoneRepository { + return new SyncTombstoneRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("sync_tombstone_repository_write"), + ); +} + export function createCurrentDismissedAlertRepository(): DismissedAlertRepository { return new DismissedAlertRepository( createCurrentRepositoryContext(), @@ -139,6 +228,20 @@ export function createCurrentFileManagerBookmarkRepository(): FileManagerBookmar ); } +export function createCurrentFleetRepository(): FleetRepository { + return new FleetRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("fleet_repository_write"), + ); +} + +export function createCurrentFleetInventoryRepository(): FleetInventoryRepository { + return new FleetInventoryRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("fleet_inventory_repository_write"), + ); +} + export function createCurrentHomepageItemRepository(): HomepageItemRepository { return new HomepageItemRepository( createCurrentRepositoryContext(), @@ -170,7 +273,9 @@ export function createCurrentHostHealthRepository(): HostHealthRepository { export function createCurrentHostMetricsHistoryRepository(): HostMetricsHistoryRepository { return new HostMetricsHistoryRepository( createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("host_metrics_history_repository_write"), + createCurrentRepositoryLazyWriteHook( + "host_metrics_history_repository_write", + ), ); } @@ -183,6 +288,15 @@ export function createCurrentHostMetricsPreferenceRepository(): HostMetricsPrefe ); } +export function createCurrentProxmoxNodeHistoryRepository(): ProxmoxNodeHistoryRepository { + return new ProxmoxNodeHistoryRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryLazyWriteHook( + "proxmox_node_history_repository_write", + ), + ); +} + export function createCurrentHostRepository(): HostRepository { return new HostRepository( createCurrentRepositoryContext(), @@ -194,6 +308,34 @@ export function createCurrentHostResolutionRepository(): HostResolutionRepositor return new HostResolutionRepository( createCurrentRepositoryContext(), createCurrentRepositoryWriteHook("host_resolution_repository_write"), + createCurrentRepositoryLazyWriteHook( + "host_resolution_repository_lazy_write", + ), + ); +} + +export function createCurrentHostSidebarPreferenceRepository(): HostSidebarPreferenceRepository { + return new HostSidebarPreferenceRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook( + "host_sidebar_preference_repository_write", + ), + ); +} + +export function createCurrentCredentialSidebarPreferenceRepository(): CredentialSidebarPreferenceRepository { + return new CredentialSidebarPreferenceRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook( + "credential_sidebar_preference_repository_write", + ), + ); +} + +export function createCurrentUiPreferenceRepository(): UiPreferenceRepository { + return new UiPreferenceRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("ui_preference_repository_write"), ); } @@ -253,6 +395,13 @@ export function createCurrentSessionRepository(): SessionRepository { ); } +export function createCurrentSessionShareRepository(): SessionShareRepository { + return new SessionShareRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("session_share_repository_write"), + ); +} + export function createCurrentSettingsRepository(): SettingsRepository { return new SettingsRepository( createCurrentRepositoryContext(), @@ -267,6 +416,15 @@ export function createCurrentSharedHostSecretsRepository(): SharedHostSecretsRep ); } +export function createCurrentSharedHostAuthOverrideRepository(): SharedHostAuthOverrideRepository { + return new SharedHostAuthOverrideRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook( + "shared_host_auth_override_repository_write", + ), + ); +} + export function createCurrentSnippetRepository(): SnippetRepository { return new SnippetRepository( createCurrentRepositoryContext(), @@ -354,3 +512,76 @@ export function createCurrentVaultTokenRepository(): VaultTokenRepository { createCurrentRepositoryWriteHook("vault_token_repository_write"), ); } + +export function createCurrentWorkspaceRepository(): WorkspaceRepository { + return new WorkspaceRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("workspace_repository_write"), + ); +} + +/** + * Loads the settings cache. Must run during startup on engines without a + * synchronous read, before anything calls getCurrentSettingValue. + */ +export async function primeCurrentSettingsCache(): Promise { + const rows = await createCurrentSettingsRepository().listAll(); + primeSettingsCache(rows); +} + +/** + * How often a replica re-reads the settings table. + * + * Override with SETTINGS_CACHE_REFRESH_SECONDS; 0 disables the refresh. + */ +const REFRESH_SECONDS_ENV = "SETTINGS_CACHE_REFRESH_SECONDS"; +const DEFAULT_REFRESH_SECONDS = 30; + +let refreshTimer: NodeJS.Timeout | null = null; + +/** + * Keeps the settings cache from drifting on a multi-replica deployment. + * + * The cache is per-process and updated in the process that writes. That is + * enough for SQLite, where there is only ever one process. On Postgres and + * MySQL โ€” which exist here precisely so more than one instance can share the + * data โ€” a setting changed on one replica would otherwise never reach the + * others, because the synchronous read has no way to go back to the database. + * + * Periodic re-priming does not make the value immediately consistent. It bounds + * how long it can be wrong, which is the difference between a setting that + * takes effect on the next tick and one that takes effect at the next restart. + */ +export function startSettingsCacheRefresh( + env = process.env, + refresh: () => Promise = primeCurrentSettingsCache, +): void { + if (refreshTimer) return; + + const seconds = refreshIntervalSeconds(env); + if (seconds === null) return; + + refreshTimer = setInterval(() => { + void refresh().catch(() => { + // A failed refresh leaves the previous values in place, which is the + // right outcome: a transient database blip should not blank the cache. + // Every caller reads a missing setting as "use the default", so an empty + // cache would silently revert configuration across the deployment. + }); + }, seconds * 1000); + + refreshTimer.unref(); +} + +/** The configured interval, or null when refreshing is switched off. */ +export function refreshIntervalSeconds(env = process.env): number | null { + const seconds = Number(env[REFRESH_SECONDS_ENV] ?? DEFAULT_REFRESH_SECONDS); + return Number.isFinite(seconds) && seconds > 0 ? seconds : null; +} + +/** Test seam. */ +export function stopSettingsCacheRefresh(): void { + if (!refreshTimer) return; + clearInterval(refreshTimer); + refreshTimer = null; +} diff --git a/src/backend/database/repositories/field-encryption-boundary.ts b/src/backend/database/repositories/field-encryption-boundary.ts deleted file mode 100644 index 04cdf02..0000000 --- a/src/backend/database/repositories/field-encryption-boundary.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { FieldCrypto } from "../../utils/field-crypto.js"; -import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js"; - -const FIELD_ENCRYPTION_POLICY = { - users: { - sensitive: new Set([ - "passwordHash", - "clientSecret", - "totpSecret", - "totpBackupCodes", - "oidcIdentifier", - ]), - plaintext: new Set(["id", "username", "isAdmin", "isOidc"]), - }, - ssh_data: { - sensitive: new Set([ - "password", - "key", - "keyPassword", - "sudoPassword", - "autostartPassword", - "autostartKey", - "autostartKeyPassword", - "socks5Password", - "rdpPassword", - "vncPassword", - "telnetPassword", - ]), - plaintext: new Set([ - "id", - "userId", - "connectionType", - "name", - "ip", - "port", - "username", - "folder", - "tags", - "authType", - "credentialId", - ]), - }, - ssh_credentials: { - sensitive: new Set([ - "password", - "key", - "privateKey", - "publicKey", - "keyPassword", - ]), - plaintext: new Set([ - "id", - "userId", - "name", - "description", - "folder", - "tags", - "authType", - "username", - "keyType", - "detectedKeyType", - "usageCount", - "lastUsed", - ]), - }, - opkssh_tokens: { - sensitive: new Set(["sshCert", "privateKey"]), - plaintext: new Set(["id", "userId", "hostId", "createdAt", "expiresAt"]), - }, - termix_identity_ca: { - sensitive: new Set(["privateKey"]), - plaintext: new Set(["id", "publicKey", "createdAt", "updatedAt"]), - }, - vault_tokens: { - sensitive: new Set(["sshCert", "privateKey"]), - plaintext: new Set(["id", "userId", "profileId", "expiresAt"]), - }, -} as const; - -type PolicyTable = keyof typeof FIELD_ENCRYPTION_POLICY; -export type FieldClassification = "sensitive" | "plaintext" | "unknown"; - -export class FieldEncryptionBoundary { - static classifyField( - tableName: string, - fieldName: string, - ): FieldClassification { - const policy = this.getPolicy(tableName); - if (!policy) return "unknown"; - if (policy.sensitive.has(fieldName)) return "sensitive"; - if (policy.plaintext.has(fieldName)) return "plaintext"; - return "unknown"; - } - - static getSensitiveFields(tableName: string): string[] { - const policy = this.getPolicy(tableName); - return policy ? [...policy.sensitive].sort() : []; - } - - static encryptRecord>( - tableName: string, - record: T, - userDataKey: Buffer, - recordId = record.id, - ): T { - const id = this.requireRecordId(recordId); - const encryptedRecord: Record = { ...record }; - - for (const fieldName of this.getSensitiveFields(tableName)) { - const value = encryptedRecord[fieldName]; - if (typeof value === "string" && value) { - encryptedRecord[fieldName] = FieldCrypto.encryptField( - value, - userDataKey, - id, - fieldName, - ); - } - } - - return encryptedRecord as T; - } - - static decryptRecord>( - tableName: string, - record: T, - userDataKey: Buffer, - recordId = record.id, - ): T { - const id = this.requireRecordId(recordId); - const decryptedRecord: Record = { ...record }; - - for (const fieldName of this.getSensitiveFields(tableName)) { - const value = decryptedRecord[fieldName]; - if (typeof value === "string" && value) { - decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue( - value, - userDataKey, - id, - fieldName, - ); - } - } - - return decryptedRecord as T; - } - - private static getPolicy(tableName: string) { - return FIELD_ENCRYPTION_POLICY[tableName as PolicyTable]; - } - - private static requireRecordId(recordId: unknown): string { - if (recordId === null || recordId === undefined || recordId === "") { - throw new Error("Field encryption requires a stable record id."); - } - return String(recordId); - } -} diff --git a/src/backend/database/repositories/file-manager-bookmark-repository.ts b/src/backend/database/repositories/file-manager-bookmark-repository.ts index dfd68b3..351b2af 100644 --- a/src/backend/database/repositories/file-manager-bookmark-repository.ts +++ b/src/backend/database/repositories/file-manager-bookmark-repository.ts @@ -5,6 +5,7 @@ import { fileManagerShortcuts, } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; export type FileManagerRecentRecord = typeof fileManagerRecent.$inferSelect; export type FileManagerPinnedRecord = typeof fileManagerPinned.$inferSelect; @@ -112,7 +113,7 @@ export class FileManagerBookmarkRepository { userId: string, input: Pick, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerRecent) .where( and( @@ -120,14 +121,13 @@ export class FileManagerBookmarkRepository { eq(fileManagerRecent.hostId, input.hostId), eq(fileManagerRecent.path, input.path), ), - ) - .returning({ id: fileManagerRecent.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async listPinnedForHost( @@ -199,7 +199,7 @@ export class FileManagerBookmarkRepository { userId: string, input: Pick, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerPinned) .where( and( @@ -207,14 +207,13 @@ export class FileManagerBookmarkRepository { eq(fileManagerPinned.hostId, input.hostId), eq(fileManagerPinned.path, input.path), ), - ) - .returning({ id: fileManagerPinned.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async listShortcutsForHost( @@ -288,7 +287,7 @@ export class FileManagerBookmarkRepository { userId: string, input: Pick, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerShortcuts) .where( and( @@ -296,14 +295,13 @@ export class FileManagerBookmarkRepository { eq(fileManagerShortcuts.hostId, input.hostId), eq(fileManagerShortcuts.path, input.path), ), - ) - .returning({ id: fileManagerShortcuts.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByUserId(userId: string): Promise { @@ -456,75 +454,66 @@ export class FileManagerBookmarkRepository { } private async deleteRecentByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerRecent) - .where(eq(fileManagerRecent.userId, userId)) - .returning({ id: fileManagerRecent.id }); - return rows.length; + .where(eq(fileManagerRecent.userId, userId)); + return rowsAffected(result); } private async deletePinnedByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerPinned) - .where(eq(fileManagerPinned.userId, userId)) - .returning({ id: fileManagerPinned.id }); - return rows.length; + .where(eq(fileManagerPinned.userId, userId)); + return rowsAffected(result); } private async deleteShortcutsByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerShortcuts) - .where(eq(fileManagerShortcuts.userId, userId)) - .returning({ id: fileManagerShortcuts.id }); - return rows.length; + .where(eq(fileManagerShortcuts.userId, userId)); + return rowsAffected(result); } private async deleteRecentByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerRecent) - .where(eq(fileManagerRecent.hostId, hostId)) - .returning({ id: fileManagerRecent.id }); - return rows.length; + .where(eq(fileManagerRecent.hostId, hostId)); + return rowsAffected(result); } private async deletePinnedByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerPinned) - .where(eq(fileManagerPinned.hostId, hostId)) - .returning({ id: fileManagerPinned.id }); - return rows.length; + .where(eq(fileManagerPinned.hostId, hostId)); + return rowsAffected(result); } private async deleteShortcutsByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerShortcuts) - .where(eq(fileManagerShortcuts.hostId, hostId)) - .returning({ id: fileManagerShortcuts.id }); - return rows.length; + .where(eq(fileManagerShortcuts.hostId, hostId)); + return rowsAffected(result); } private async deleteRecentByHostIds(hostIds: number[]): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerRecent) - .where(inArray(fileManagerRecent.hostId, hostIds)) - .returning({ id: fileManagerRecent.id }); - return rows.length; + .where(inArray(fileManagerRecent.hostId, hostIds)); + return rowsAffected(result); } private async deletePinnedByHostIds(hostIds: number[]): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerPinned) - .where(inArray(fileManagerPinned.hostId, hostIds)) - .returning({ id: fileManagerPinned.id }); - return rows.length; + .where(inArray(fileManagerPinned.hostId, hostIds)); + return rowsAffected(result); } private async deleteShortcutsByHostIds(hostIds: number[]): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(fileManagerShortcuts) - .where(inArray(fileManagerShortcuts.hostId, hostIds)) - .returning({ id: fileManagerShortcuts.id }); - return rows.length; + .where(inArray(fileManagerShortcuts.hostId, hostIds)); + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/fleet-inventory-repository.ts b/src/backend/database/repositories/fleet-inventory-repository.ts new file mode 100644 index 0000000..f016911 --- /dev/null +++ b/src/backend/database/repositories/fleet-inventory-repository.ts @@ -0,0 +1,90 @@ +import { and, eq, inArray } from "drizzle-orm"; +import { fleetInventory } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { insertReturning, updateReturning } from "./returning.js"; + +export type FleetInventoryRecord = typeof fleetInventory.$inferSelect; + +export interface FleetInventoryInput { + osPrettyName: string | null; + kernel: string | null; + architecture: string | null; + hostname: string | null; + uptimeSeconds: number | null; + ip: string | null; + packageManager: string | null; +} + +export class FleetInventoryRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async upsert( + userId: string, + hostId: number, + input: FleetInventoryInput, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findOne(userId, hostId); + + if (existing) { + const [updated] = await updateReturning( + this.context, + fleetInventory, + { ...input, collectedAt: now }, + eq(fleetInventory.id, existing.id), + ); + await this.afterWrite(); + return updated; + } + + const [created] = await insertReturning(this.context, fleetInventory, { + userId, + hostId, + ...input, + collectedAt: now, + }); + + await this.afterWrite(); + return created; + } + + async findOne( + userId: string, + hostId: number, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(fleetInventory) + .where( + and( + eq(fleetInventory.userId, userId), + eq(fleetInventory.hostId, hostId), + ), + ) + .limit(1); + return rows[0] ?? null; + } + + async listForHosts( + userId: string, + hostIds: number[], + ): Promise { + if (hostIds.length === 0) return []; + return this.context.drizzle + .select() + .from(fleetInventory) + .where( + and( + eq(fleetInventory.userId, userId), + inArray(fleetInventory.hostId, hostIds), + ), + ); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/fleet-repository.ts b/src/backend/database/repositories/fleet-repository.ts new file mode 100644 index 0000000..2761bd5 --- /dev/null +++ b/src/backend/database/repositories/fleet-repository.ts @@ -0,0 +1,235 @@ +import { and, eq } from "drizzle-orm"; +import { randomUUID } from "crypto"; +import { fleets, fleetMembers, hosts } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; + +export type FleetRecord = typeof fleets.$inferSelect; +export type FleetMemberHostRecord = typeof hosts.$inferSelect; + +export interface FleetCreateInput { + name: string; + description?: string | null; + color?: string | null; + icon?: string | null; + tagRules?: string[]; +} + +export interface FleetUpdateInput { + name?: string; + description?: string | null; + color?: string | null; + icon?: string | null; + tagRules?: string[]; +} + +function parseTagRules(raw: string | null): string[] { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) + ? parsed.filter((t): t is string => typeof t === "string") + : []; + } catch { + return []; + } +} + +export class FleetRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async listByUser(userId: string): Promise { + return this.context.drizzle + .select() + .from(fleets) + .where(eq(fleets.userId, userId)); + } + + async findById(userId: string, fleetId: number): Promise { + const rows = await this.context.drizzle + .select() + .from(fleets) + .where(and(eq(fleets.id, fleetId), eq(fleets.userId, userId))) + .limit(1); + return rows[0] ?? null; + } + + async create( + userId: string, + input: FleetCreateInput, + now = new Date().toISOString(), + ): Promise { + const [created] = await insertReturning(this.context, fleets, { + userId, + name: input.name, + description: input.description ?? null, + color: input.color ?? null, + icon: input.icon ?? null, + tagRules: input.tagRules ? JSON.stringify(input.tagRules) : null, + syncId: randomUUID(), + createdAt: now, + updatedAt: now, + }); + + await this.afterWrite(); + return created; + } + + async update( + userId: string, + fleetId: number, + input: FleetUpdateInput, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findById(userId, fleetId); + if (!existing) return null; + + const [updated] = await updateReturning( + this.context, + fleets, + { + name: input.name ?? existing.name, + description: + input.description === undefined + ? existing.description + : input.description, + color: input.color === undefined ? existing.color : input.color, + icon: input.icon === undefined ? existing.icon : input.icon, + tagRules: + input.tagRules === undefined + ? existing.tagRules + : JSON.stringify(input.tagRules), + updatedAt: now, + }, + and(eq(fleets.id, fleetId), eq(fleets.userId, userId)), + ); + + await this.afterWrite(); + return updated ?? null; + } + + async delete(userId: string, fleetId: number): Promise { + const deleted = await deleteReturning( + this.context, + fleets, + and(eq(fleets.id, fleetId), eq(fleets.userId, userId)), + ); + + if (deleted.length > 0) { + await this.afterWrite(); + return true; + } + return false; + } + + async addMember( + fleetId: number, + hostId: number, + now = new Date().toISOString(), + ): Promise { + const existing = await this.context.drizzle + .select({ id: fleetMembers.id }) + .from(fleetMembers) + .where( + and(eq(fleetMembers.fleetId, fleetId), eq(fleetMembers.hostId, hostId)), + ) + .limit(1); + + if (existing.length > 0) return; + + await this.context.drizzle.insert(fleetMembers).values({ + fleetId, + hostId, + addedAt: now, + }); + + await this.afterWrite(); + } + + async removeMember(fleetId: number, hostId: number): Promise { + const result = await this.context.drizzle + .delete(fleetMembers) + .where( + and(eq(fleetMembers.fleetId, fleetId), eq(fleetMembers.hostId, hostId)), + ); + + const affected = rowsAffected(result) > 0; + if (affected) await this.afterWrite(); + return affected; + } + + async listStaticMemberIds(fleetId: number): Promise { + const rows = await this.context.drizzle + .select({ hostId: fleetMembers.hostId }) + .from(fleetMembers) + .where(eq(fleetMembers.fleetId, fleetId)); + return rows.map((r) => r.hostId); + } + + /** + * Effective membership = static fleetMembers rows union hosts whose + * comma-separated tags string intersects any tag in the fleet's tagRules. + * Both sets are scoped to the fleet owner's hosts and deduplicated by id. + */ + async listEffectiveMembers( + userId: string, + fleetId: number, + ): Promise { + const fleet = await this.findById(userId, fleetId); + if (!fleet) return []; + + const staticIds = await this.listStaticMemberIds(fleetId); + const tagRules = parseTagRules(fleet.tagRules); + + const ownedHosts = await this.context.drizzle + .select() + .from(hosts) + .where(eq(hosts.userId, userId)); + + const byId = new Map(); + + for (const host of ownedHosts) { + if (staticIds.includes(host.id)) { + byId.set(host.id, host); + continue; + } + if (tagRules.length === 0) continue; + const hostTags = (host.tags ?? "") + .split(",") + .map((t) => t.trim()) + .filter(Boolean); + if (hostTags.some((tag) => tagRules.includes(tag))) { + byId.set(host.id, host); + } + } + + // Static members can reference hosts the fleet owner no longer owns + // (rare, but the FK cascade only fires on host delete, not transfer) - + // filter those out rather than surface a partial/foreign row. + return Array.from(byId.values()); + } + + async deleteByUserId(userId: string): Promise { + const userFleets = await this.listByUser(userId); + const result = await this.context.drizzle + .delete(fleets) + .where(eq(fleets.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + return userFleets.length; + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/homepage-item-repository.ts b/src/backend/database/repositories/homepage-item-repository.ts index 1dc8efe..7f4a88c 100644 --- a/src/backend/database/repositories/homepage-item-repository.ts +++ b/src/backend/database/repositories/homepage-item-repository.ts @@ -1,6 +1,13 @@ import { and, asc, eq } from "drizzle-orm"; +import { randomUUID } from "crypto"; import { homepageItems } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; export type HomepageItemRecord = typeof homepageItems.$inferSelect; @@ -34,17 +41,15 @@ export class HomepageItemRepository { input: HomepageItemCreateInput, now = new Date().toISOString(), ): Promise { - const [created] = await this.context.drizzle - .insert(homepageItems) - .values({ - userId, - typeId: input.typeId, - title: input.title, - config: input.config, - createdAt: now, - updatedAt: now, - }) - .returning(); + const [created] = await insertReturning(this.context, homepageItems, { + syncId: randomUUID(), + userId, + typeId: input.typeId, + title: input.title, + config: input.config, + createdAt: now, + updatedAt: now, + }); await this.afterWrite(); return created; @@ -69,11 +74,12 @@ export class HomepageItemRepository { updates: HomepageItemUpdateInput, updatedAt = new Date().toISOString(), ): Promise { - const [updated] = await this.context.drizzle - .update(homepageItems) - .set({ ...updates, updatedAt }) - .where(and(eq(homepageItems.id, id), eq(homepageItems.userId, userId))) - .returning(); + const [updated] = await updateReturning( + this.context, + homepageItems, + { ...updates, updatedAt }, + and(eq(homepageItems.id, id), eq(homepageItems.userId, userId)), + ); if (updated) { await this.afterWrite(); @@ -82,30 +88,31 @@ export class HomepageItemRepository { return updated ?? null; } - async deleteForUser(userId: string, id: number): Promise { - const rows = await this.context.drizzle - .delete(homepageItems) - .where(and(eq(homepageItems.id, id), eq(homepageItems.userId, userId))) - .returning({ id: homepageItems.id }); + async deleteForUser( + userId: string, + id: number, + ): Promise<{ syncId: string | null } | null> { + const rows = await deleteReturning( + this.context, + homepageItems, + and(eq(homepageItems.id, id), eq(homepageItems.userId, userId)), + ); - if (rows.length > 0) { - await this.afterWrite(); - } - - return rows.length > 0; + if (rows.length === 0) return null; + await this.afterWrite(); + return { syncId: rows[0].syncId }; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(homepageItems) - .where(eq(homepageItems.userId, userId)) - .returning({ id: homepageItems.id }); + .where(eq(homepageItems.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/homepage-layout-repository.ts b/src/backend/database/repositories/homepage-layout-repository.ts index bb10c45..d1e2d8c 100644 --- a/src/backend/database/repositories/homepage-layout-repository.ts +++ b/src/backend/database/repositories/homepage-layout-repository.ts @@ -1,6 +1,8 @@ import { eq } from "drizzle-orm"; import { homepageLayouts } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; export type HomepageLayoutRecord = typeof homepageLayouts.$inferSelect; @@ -28,34 +30,35 @@ export class HomepageLayoutRepository { const existing = await this.findByUserId(userId); if (!existing) { - const [created] = await this.context.drizzle - .insert(homepageLayouts) - .values({ userId, layout, updatedAt }) - .returning(); + const [created] = await insertReturning(this.context, homepageLayouts, { + userId, + layout, + updatedAt, + }); await this.afterWrite(); return created; } - const [updated] = await this.context.drizzle - .update(homepageLayouts) - .set({ layout, updatedAt }) - .where(eq(homepageLayouts.userId, userId)) - .returning(); + const [updated] = await updateReturning( + this.context, + homepageLayouts, + { layout, updatedAt }, + eq(homepageLayouts.userId, userId), + ); await this.afterWrite(); return updated; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(homepageLayouts) - .where(eq(homepageLayouts.userId, userId)) - .returning({ id: homepageLayouts.id }); + .where(eq(homepageLayouts.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/host-folder-repository.ts b/src/backend/database/repositories/host-folder-repository.ts index 50d0b16..5e43093 100644 --- a/src/backend/database/repositories/host-folder-repository.ts +++ b/src/backend/database/repositories/host-folder-repository.ts @@ -1,7 +1,14 @@ import { and, eq, like, or, sql } from "drizzle-orm"; +import { randomUUID } from "crypto"; import type { SQLiteColumn } from "drizzle-orm/sqlite-core"; import { hosts, sshCredentials, sshFolders } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; export type HostFolderRecord = typeof sshFolders.$inferSelect; export type HostFolderHostRecord = typeof hosts.$inferSelect; @@ -23,19 +30,31 @@ export class HostFolderRepository { newName: string, now = new Date().toISOString(), ): Promise { + // CAST target: every engine spells the text type differently enough to + // matter here โ€” MySQL has no `text` cast and wants `char`. + const textType = this.context.dialect === "mysql" ? "char" : "text"; const oldPrefix = `${oldName} / `; const newPrefix = `${newName} / `; const childLike = `${oldPrefix}%`; + // CONCAT, not `||`: MySQL reads `||` as logical OR unless the server runs + // with PIPES_AS_CONCAT, so the child paths would have been rewritten to 0. + // No error, just wrong folder names. CONCAT and SUBSTR mean the same thing + // on all three engines. + // + // The prefix is inlined rather than bound: CONCAT is variadic, so Postgres + // cannot infer a parameter's type from its position and rejects the + // statement with 42P18 before it runs. The value is a folder name the + // caller supplied, so it goes through a bound placeholder in a plain + // concatenation instead of sql.raw. const renameExpr = (col: SQLiteColumn) => - sql`CASE WHEN ${col} = ${oldName} THEN ${newName} ELSE ${newPrefix} || substr(${col}, ${oldPrefix.length + 1}) END`; + sql`CASE WHEN ${col} = ${oldName} THEN ${newName} ELSE CONCAT(CAST(${newPrefix} AS ${sql.raw(textType)}), SUBSTR(${col}, ${sql.raw(String(oldPrefix.length + 1))})) END`; const folderMatch = (col: SQLiteColumn) => or(eq(col, oldName), like(col, childLike)); const updatedHosts = await this.context.drizzle .update(hosts) .set({ folder: renameExpr(hosts.folder), updatedAt: now }) - .where(and(eq(hosts.userId, userId), folderMatch(hosts.folder))) - .returning({ id: hosts.id }); + .where(and(eq(hosts.userId, userId), folderMatch(hosts.folder))); const updatedCredentials = await this.context.drizzle .update(sshCredentials) @@ -45,8 +64,7 @@ export class HostFolderRepository { eq(sshCredentials.userId, userId), folderMatch(sshCredentials.folder), ), - ) - .returning({ id: sshCredentials.id }); + ); await this.context.drizzle .update(sshFolders) @@ -55,8 +73,8 @@ export class HostFolderRepository { await this.afterWrite(); return { - updatedHosts: updatedHosts.length, - updatedCredentials: updatedCredentials.length, + updatedHosts: rowsAffected(updatedHosts), + updatedCredentials: rowsAffected(updatedCredentials), }; } @@ -72,36 +90,121 @@ export class HostFolderRepository { name: string, color: string | null | undefined, icon: string | null | undefined, + credentialId?: number | null, now = new Date().toISOString(), ): Promise<{ folder: HostFolderRecord; created: boolean }> { const existing = await this.findFolder(userId, name); if (existing) { - const [updated] = await this.context.drizzle - .update(sshFolders) - .set({ color, icon, updatedAt: now }) - .where(and(eq(sshFolders.userId, userId), eq(sshFolders.name, name))) - .returning(); + const [updated] = await updateReturning( + this.context, + sshFolders, + { + color, + icon, + credentialId: + credentialId === undefined ? existing.credentialId : credentialId, + updatedAt: now, + }, + and(eq(sshFolders.userId, userId), eq(sshFolders.name, name)), + ); await this.afterWrite(); return { folder: updated, created: false }; } - const [created] = await this.context.drizzle - .insert(sshFolders) - .values({ - userId, - name, - color, - icon, - createdAt: now, - updatedAt: now, - }) - .returning(); + const [created] = await insertReturning(this.context, sshFolders, { + syncId: randomUUID(), + userId, + name, + color, + icon, + credentialId: credentialId ?? null, + createdAt: now, + updatedAt: now, + }); await this.afterWrite(); return { folder: created, created: true }; } + /** + * Sets a distinct manual sortOrder per sibling folder (drag-to-reorder). + * Folders with no existing sshFolders row are created first (matching the + * empty-folder-persists behavior elsewhere) so the order survives even for + * folders that only ever existed implicitly via host paths. + */ + async reorderFolders( + userId: string, + positions: { name: string; sortOrder: number }[], + now = new Date().toISOString(), + ): Promise { + if (positions.length === 0) return 0; + + let affected: number; + if (this.context.dialect === "sqlite") { + affected = this.context.drizzle.transaction((tx) => { + let count = 0; + for (const { name, sortOrder } of positions) { + const result = tx + .update(sshFolders) + .set({ sortOrder, updatedAt: now }) + .where( + and(eq(sshFolders.userId, userId), eq(sshFolders.name, name)), + ) + .run(); + if (rowsAffected(result) > 0) { + count += rowsAffected(result); + continue; + } + tx.insert(sshFolders) + .values({ + syncId: randomUUID(), + userId, + name, + sortOrder, + createdAt: now, + updatedAt: now, + }) + .run(); + count += 1; + } + return count; + }); + } else { + affected = await this.context.drizzle.transaction(async (tx) => { + let count = 0; + for (const { name, sortOrder } of positions) { + const result = await tx + .update(sshFolders) + .set({ sortOrder, updatedAt: now }) + .where( + and(eq(sshFolders.userId, userId), eq(sshFolders.name, name)), + ); + if (rowsAffected(result) > 0) { + count += rowsAffected(result); + continue; + } + await tx.insert(sshFolders).values({ + syncId: randomUUID(), + userId, + name, + sortOrder, + createdAt: now, + updatedAt: now, + }); + count += 1; + } + return count; + }); + } + + if (affected > 0) { + await this.afterWrite(); + } + + return affected; + } + async listHostsInFolder( userId: string, folderName: string, @@ -118,7 +221,7 @@ export class HostFolderRepository { async deleteHostsAndFolderRecords( userId: string, folderName: string, - ): Promise { + ): Promise<{ hostSyncIds: string[]; folderSyncIds: string[] }> { const folderMatch = (col: SQLiteColumn) => or(eq(col, folderName), like(col, `${folderName} / %`)); @@ -129,24 +232,34 @@ export class HostFolderRepository { .where(and(eq(hosts.userId, userId), folderMatch(hosts.folder))); } - await this.context.drizzle - .delete(sshFolders) - .where(and(eq(sshFolders.userId, userId), folderMatch(sshFolders.name))); + const deletedFolders = await deleteReturning( + this.context, + sshFolders, + and(eq(sshFolders.userId, userId), folderMatch(sshFolders.name)), + ); await this.afterWrite(); + + return { + hostSyncIds: hostsToDelete + .map((h) => h.syncId) + .filter((id): id is string => !!id), + folderSyncIds: deletedFolders + .map((f) => f.syncId) + .filter((id): id is string => !!id), + }; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sshFolders) - .where(eq(sshFolders.userId, userId)) - .returning({ id: sshFolders.id }); + .where(eq(sshFolders.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async findFolder( diff --git a/src/backend/database/repositories/host-health-repository.ts b/src/backend/database/repositories/host-health-repository.ts index 5aac930..875e1d7 100644 --- a/src/backend/database/repositories/host-health-repository.ts +++ b/src/backend/database/repositories/host-health-repository.ts @@ -1,6 +1,8 @@ -import { and, desc, eq } from "drizzle-orm"; +import { and, desc, eq, notInArray } from "drizzle-orm"; import { hostHealthChecks, hostHealthHistory } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; export type HostHealthCheckRecord = typeof hostHealthChecks.$inferSelect; export type HostHealthHistoryRecord = typeof hostHealthHistory.$inferSelect; @@ -45,27 +47,25 @@ export class HostHealthRepository { ): Promise { const existing = await this.findChecksByUserAndHost(userId, hostId); if (existing) { - const [updated] = await this.context.drizzle - .update(hostHealthChecks) - .set({ checks, intervalSeconds, updatedAt: now }) - .where(eq(hostHealthChecks.id, existing.id)) - .returning(); + const [updated] = await updateReturning( + this.context, + hostHealthChecks, + { checks, intervalSeconds, updatedAt: now }, + eq(hostHealthChecks.id, existing.id), + ); await this.afterWrite(); return updated; } - const [created] = await this.context.drizzle - .insert(hostHealthChecks) - .values({ - userId, - hostId, - checks, - intervalSeconds, - createdAt: now, - updatedAt: now, - }) - .returning(); + const [created] = await insertReturning(this.context, hostHealthChecks, { + userId, + hostId, + checks, + intervalSeconds, + createdAt: now, + updatedAt: now, + }); await this.afterWrite(); return created; @@ -94,7 +94,7 @@ export class HostHealthRepository { })), ); - this.pruneHistory(userId, hostId, keep); + await this.pruneHistory(userId, hostId, keep); await this.afterWrite(); return results.length; } @@ -121,41 +121,55 @@ export class HostHealthRepository { checksDeleted: number; historyDeleted: number; }> { - const historyRows = await this.context.drizzle + const historyResult = await this.context.drizzle .delete(hostHealthHistory) - .where(eq(hostHealthHistory.userId, userId)) - .returning({ id: hostHealthHistory.id }); + .where(eq(hostHealthHistory.userId, userId)); - const checkRows = await this.context.drizzle + const result = await this.context.drizzle .delete(hostHealthChecks) - .where(eq(hostHealthChecks.userId, userId)) - .returning({ id: hostHealthChecks.id }); + .where(eq(hostHealthChecks.userId, userId)); - if (historyRows.length > 0 || checkRows.length > 0) { + if (rowsAffected(historyResult) > 0 || rowsAffected(result) > 0) { await this.afterWrite(); } return { - checksDeleted: checkRows.length, - historyDeleted: historyRows.length, + checksDeleted: rowsAffected(result), + historyDeleted: rowsAffected(historyResult), }; } - private pruneHistory(userId: string, hostId: number, keep: number): void { - this.context.sqlite - ?.prepare( - `DELETE FROM host_health_history - WHERE id IN ( - SELECT id FROM host_health_history - WHERE user_id = ? AND host_id = ? - AND id NOT IN ( - SELECT id FROM host_health_history - WHERE user_id = ? AND host_id = ? - ORDER BY ts DESC LIMIT ? - ) - )`, - ) - .run(userId, hostId, userId, hostId, keep); + /** Keeps the newest `keep` rows for the host and drops the rest. */ + private async pruneHistory( + userId: string, + hostId: number, + keep: number, + ): Promise { + const scope = and( + eq(hostHealthHistory.userId, userId), + eq(hostHealthHistory.hostId, hostId), + ); + + const retained = await this.context.drizzle + .select({ id: hostHealthHistory.id }) + .from(hostHealthHistory) + .where(scope) + .orderBy(desc(hostHealthHistory.ts)) + .limit(keep); + + // Nothing retained means nothing to keep back, so the scope alone is the + // delete condition. + await this.context.drizzle.delete(hostHealthHistory).where( + retained.length + ? and( + scope, + notInArray( + hostHealthHistory.id, + retained.map((row) => row.id), + ), + ) + : scope, + ); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/host-metrics-history-repository.ts b/src/backend/database/repositories/host-metrics-history-repository.ts index cbad4e2..62bdb0e 100644 --- a/src/backend/database/repositories/host-metrics-history-repository.ts +++ b/src/backend/database/repositories/host-metrics-history-repository.ts @@ -1,6 +1,7 @@ -import { and, asc, eq, gte, lte } from "drizzle-orm"; +import { and, asc, eq, gte, lt, lte } from "drizzle-orm"; import { hostMetricsHistory } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { sqlTimestampDaysAgo } from "./sql-timestamp.js"; export type HostMetricsHistoryRecord = typeof hostMetricsHistory.$inferSelect; @@ -32,12 +33,15 @@ export class HostMetricsHistoryRepository { await this.afterWrite(); } - pruneOlderThan(hostId: number, retentionDays: number): void { - this.context.sqlite - ?.prepare( - "DELETE FROM host_metrics_history WHERE host_id = ? AND ts < datetime('now', ?)", - ) - .run(hostId, `-${retentionDays} days`); + async pruneOlderThan(hostId: number, retentionDays: number): Promise { + await this.context.drizzle + .delete(hostMetricsHistory) + .where( + and( + eq(hostMetricsHistory.hostId, hostId), + lt(hostMetricsHistory.ts, sqlTimestampDaysAgo(retentionDays)), + ), + ); } async listRange( diff --git a/src/backend/database/repositories/host-metrics-preference-repository.ts b/src/backend/database/repositories/host-metrics-preference-repository.ts index 070063d..108acf8 100644 --- a/src/backend/database/repositories/host-metrics-preference-repository.ts +++ b/src/backend/database/repositories/host-metrics-preference-repository.ts @@ -1,6 +1,8 @@ import { and, eq } from "drizzle-orm"; import { hostMetricsPreferences, hosts } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; export type HostMetricsPreferenceRecord = typeof hostMetricsPreferences.$inferSelect; @@ -37,26 +39,28 @@ export class HostMetricsPreferenceRepository { ): Promise { const existing = await this.findByUserAndHost(userId, hostId); if (existing) { - const [updated] = await this.context.drizzle - .update(hostMetricsPreferences) - .set({ layout, updatedAt: now }) - .where(eq(hostMetricsPreferences.id, existing.id)) - .returning(); + const [updated] = await updateReturning( + this.context, + hostMetricsPreferences, + { layout, updatedAt: now }, + eq(hostMetricsPreferences.id, existing.id), + ); await this.afterWrite(); return updated; } - const [created] = await this.context.drizzle - .insert(hostMetricsPreferences) - .values({ + const [created] = await insertReturning( + this.context, + hostMetricsPreferences, + { userId, hostId, layout, createdAt: now, updatedAt: now, - }) - .returning(); + }, + ); await this.afterWrite(); return created; @@ -67,28 +71,26 @@ export class HostMetricsPreferenceRepository { hostId: number, statsConfig: string, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(hosts) .set({ statsConfig }) - .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))) - .returning({ id: hosts.id }); + .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))); - if (rows.length === 0) return false; + if (rowsAffected(result) === 0) return false; await this.afterWrite(); return true; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(hostMetricsPreferences) - .where(eq(hostMetricsPreferences.userId, userId)) - .returning({ id: hostMetricsPreferences.id }); + .where(eq(hostMetricsPreferences.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/host-repository.ts b/src/backend/database/repositories/host-repository.ts index 0cb7a4d..6327c9b 100644 --- a/src/backend/database/repositories/host-repository.ts +++ b/src/backend/database/repositories/host-repository.ts @@ -1,7 +1,14 @@ -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; +import { randomUUID } from "crypto"; import { hostAccess, hosts } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; export type HostRecord = typeof hosts.$inferSelect; export type NewHostRecord = typeof hosts.$inferInsert; @@ -20,10 +27,10 @@ export class HostRepository { ) {} async create(host: NewHostRecord): Promise { - const rows = await this.context.drizzle - .insert(hosts) - .values(host) - .returning(); + const rows = await insertReturning(this.context, hosts, { + syncId: randomUUID(), + ...host, + }); await this.afterWrite(); return rows[0]; } @@ -34,7 +41,11 @@ export class HostRepository { ): Promise { const userDataKey = DataCrypto.validateUserAccess(userId); const tempId = host.id ?? Date.now(); - const dataWithTempId = { ...host, id: tempId }; + const dataWithTempId = { + syncId: randomUUID(), + ...host, + id: tempId, + }; const encryptedHost = DataCrypto.encryptRecord( "ssh_data", dataWithTempId, @@ -46,10 +57,11 @@ export class HostRepository { delete (encryptedHost as Partial).id; } - const rows = await this.context.drizzle - .insert(hosts) - .values(encryptedHost as NewHostRecord) - .returning(); + const rows = await insertReturning( + this.context, + hosts, + encryptedHost as NewHostRecord, + ); await this.afterWrite(); return DataCrypto.decryptRecord("ssh_data", rows[0], userId, userDataKey); @@ -145,11 +157,12 @@ export class HostRepository { hostId: number, update: HostUpdate, ): Promise { - const rows = await this.context.drizzle - .update(hosts) - .set(update) - .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))) - .returning(); + const rows = await updateReturning( + this.context, + hosts, + { ...update, updatedAt: sql`CURRENT_TIMESTAMP` }, + and(eq(hosts.id, hostId), eq(hosts.userId, userId)), + ); await this.afterWrite(); return rows[0] ?? null; @@ -168,11 +181,12 @@ export class HostRepository { userDataKey, ); - const rows = await this.context.drizzle - .update(hosts) - .set(encryptedUpdate) - .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))) - .returning(); + const rows = await updateReturning( + this.context, + hosts, + { ...encryptedUpdate, updatedAt: sql`CURRENT_TIMESTAMP` }, + and(eq(hosts.id, hostId), eq(hosts.userId, userId)), + ); await this.afterWrite(); return rows[0] @@ -208,55 +222,102 @@ export class HostRepository { return 0; } - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(hosts) - .set(update) - .where(and(inArray(hosts.id, hostIds), eq(hosts.userId, userId))) - .returning({ id: hosts.id }); + .set({ ...update, updatedAt: sql`CURRENT_TIMESTAMP` }) + .where(and(inArray(hosts.id, hostIds), eq(hosts.userId, userId))); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } - async deleteForUser(userId: string, hostId: number): Promise { + /** + * Sets a distinct manual sortOrder per host (drag-to-reorder). Unlike + * updateManyForUser, each id gets its own value, so this is one UPDATE per + * row rather than a single set-for-all-matching-ids statement. + */ + async reorderForUser( + userId: string, + positions: { id: number; sortOrder: number }[], + ): Promise { + if (positions.length === 0) return 0; + + let affected: number; + if (this.context.dialect === "sqlite") { + affected = this.context.drizzle.transaction((tx) => { + let count = 0; + for (const { id, sortOrder } of positions) { + const result = tx + .update(hosts) + .set({ sortOrder, updatedAt: sql`CURRENT_TIMESTAMP` }) + .where(and(eq(hosts.id, id), eq(hosts.userId, userId))) + .run(); + count += rowsAffected(result); + } + return count; + }); + } else { + affected = await this.context.drizzle.transaction(async (tx) => { + let count = 0; + for (const { id, sortOrder } of positions) { + const result = await tx + .update(hosts) + .set({ sortOrder, updatedAt: sql`CURRENT_TIMESTAMP` }) + .where(and(eq(hosts.id, id), eq(hosts.userId, userId))); + count += rowsAffected(result); + } + return count; + }); + } + + if (affected > 0) { + await this.afterWrite(); + } + + return affected; + } + + async deleteForUser( + userId: string, + hostId: number, + ): Promise<{ syncId: string | null } | null> { await this.deleteAccessForHost(hostId); - const rows = await this.context.drizzle - .delete(hosts) - .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))) - .returning({ id: hosts.id }); + const rows = await deleteReturning( + this.context, + hosts, + and(eq(hosts.id, hostId), eq(hosts.userId, userId)), + ); await this.afterWrite(); - return rows.length > 0; + return rows[0] ? { syncId: rows[0].syncId } : null; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(hosts) - .where(eq(hosts.userId, userId)) - .returning({ id: hosts.id }); + .where(eq(hosts.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteAccessForHost(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(hostAccess) - .where(eq(hostAccess.hostId, hostId)) - .returning({ id: hostAccess.id }); + .where(eq(hostAccess.hostId, hostId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/host-resolution-repository.ts b/src/backend/database/repositories/host-resolution-repository.ts index 61288d5..8301318 100644 --- a/src/backend/database/repositories/host-resolution-repository.ts +++ b/src/backend/database/repositories/host-resolution-repository.ts @@ -1,5 +1,5 @@ import { and, eq, inArray, isNotNull } from "drizzle-orm"; -import { hostAccess, hosts, sshCredentials } from "../db/schema.js"; +import { hosts, sshCredentials, sshFolders } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; @@ -20,12 +20,36 @@ export interface HostUpdateStateRecord { telnetCredentialId: number | null; vaultProfileId: number | null; authType: string; + parentHostId: number | null; + folder: string | null; } export interface HostListAccessEntry { hostId: number; permissionLevel: string; expiresAt: string | null; } + +const HOST_PERMISSION_RANK: Record = { + connect: 1, + view: 2, + edit: 3, + manage: 4, +}; + +function preferHostAccess( + current: HostListAccessEntry, + candidate: HostListAccessEntry, +): HostListAccessEntry { + const currentRank = HOST_PERMISSION_RANK[current.permissionLevel] ?? 0; + const candidateRank = HOST_PERMISSION_RANK[candidate.permissionLevel] ?? 0; + if (candidateRank !== currentRank) { + return candidateRank > currentRank ? candidate : current; + } + if (current.expiresAt === null) return current; + if (candidate.expiresAt === null) return candidate; + return candidate.expiresAt > current.expiresAt ? candidate : current; +} + export type HostListRow = HostResolutionHostRecord & { ownerId: string; isShared: boolean; @@ -37,6 +61,11 @@ export class HostResolutionRepository { constructor( private readonly context: DatabaseContext, private readonly onWrite?: () => void | Promise, + // Informational only -- touchHostKeyLastVerified fires on every SSH + // connect and does not need an immediate encrypted-file rewrite the way + // storeHostKey/updateHostKey do. Keeping it separate from onWrite lets + // those two stay on the immediate forceSave path. + private readonly onLazyWrite?: () => void | Promise, ) {} async findHostById( @@ -52,6 +81,24 @@ export class HostResolutionRepository { return this.decryptOne("ssh_data", rows[0], userId); } + /** + * Translates a sync identity into this database's own row id. + * + * Deliberately not scoped to a user: `sync_id` is unique across the table, + * and a host shared with the caller belongs to someone else. Whether the + * caller may reach the row is decided by the permission check that follows, + * not here. + */ + async findHostIdBySyncId(syncId: string): Promise { + const rows = await this.context.drizzle + .select({ id: hosts.id }) + .from(hosts) + .where(eq(hosts.syncId, syncId)) + .limit(1); + + return rows[0]?.id ?? null; + } + async findHostByIdForUser( hostId: number, userId: string, @@ -77,6 +124,8 @@ export class HostResolutionRepository { telnetCredentialId: hosts.telnetCredentialId, vaultProfileId: hosts.vaultProfileId, authType: hosts.authType, + parentHostId: hosts.parentHostId, + folder: hosts.folder, }) .from(hosts) .where(eq(hosts.id, hostId)) @@ -85,6 +134,20 @@ export class HostResolutionRepository { return rows[0] ?? null; } + /** + * Minimal (id, parentHostId) rows for every host a user owns, used to walk + * ancestor chains when validating a sub-host parent assignment for cycles. + * No decryption needed -- parentHostId is a plain, unencrypted integer. + */ + async listOwnHostParentLinks( + userId: string, + ): Promise<{ id: number; parentHostId: number | null }[]> { + return this.context.drizzle + .select({ id: hosts.id, parentHostId: hosts.parentHostId }) + .from(hosts) + .where(eq(hosts.userId, userId)); + } + async findHostsByUserId(userId: string): Promise { const rows = await this.context.drizzle .select() @@ -103,9 +166,15 @@ export class HostResolutionRepository { .from(hosts) .where(eq(hosts.userId, userId)); - const sharedHostIds = Array.from( - new Set(accessEntries.map((access) => access.hostId)), - ); + const accessByHostId = new Map(); + for (const access of accessEntries) { + const current = accessByHostId.get(access.hostId); + accessByHostId.set( + access.hostId, + current ? preferHostAccess(current, access) : access, + ); + } + const sharedHostIds = Array.from(accessByHostId.keys()); const sharedHostRows = sharedHostIds.length > 0 ? await this.context.drizzle @@ -125,7 +194,7 @@ export class HostResolutionRepository { permissionLevel: undefined, expiresAt: undefined, })), - ...accessEntries.flatMap((access) => { + ...Array.from(accessByHostId.values()).flatMap((access) => { const host = sharedHostsById.get(access.hostId); if (!host || host.userId === userId) { return []; @@ -154,6 +223,22 @@ export class HostResolutionRepository { return rows[0]?.ownerId ?? null; } + /** + * Ids of the hosts this user owns, as a set. + * + * Callers that need to check ownership of many hosts at once (the status + * poll being the hot one) would otherwise issue isHostOwnedByUser per host, + * which is a query each and repeats on every poll. + */ + async listOwnedHostIds(userId: string): Promise> { + const rows = await this.context.drizzle + .select({ id: hosts.id }) + .from(hosts) + .where(eq(hosts.userId, userId)); + + return new Set(rows.map((row) => row.id)); + } + async isHostOwnedByUser(hostId: number, userId: string): Promise { const rows = await this.context.drizzle .select({ id: hosts.id }) @@ -262,7 +347,7 @@ export class HostResolutionRepository { .update(hosts) .set({ hostKeyLastVerified: now }) .where(eq(hosts.id, hostId)); - await this.afterWrite(); + await (this.onLazyWrite?.() ?? this.afterWrite()); } async findCredentialByIdForUser( @@ -283,6 +368,43 @@ export class HostResolutionRepository { return this.decryptOne("ssh_credentials", rows[0], userId); } + /** + * Batch form of findCredentialByIdForUser. + * + * The host list resolves a credential for every host it returns; issued one + * id at a time that is a query and a decrypt per host, which is the dominant + * cost of the list once an install has more than a few hundred of them. + */ + async listCredentialsByIdsForUser( + credentialIds: number[], + userId: string, + ): Promise> { + const unique = Array.from(new Set(credentialIds)); + if (unique.length === 0) return new Map(); + + const rows = await this.context.drizzle + .select() + .from(sshCredentials) + .where( + and( + inArray(sshCredentials.id, unique), + eq(sshCredentials.userId, userId), + ), + ); + + const userDataKey = DataCrypto.getUserDataKey(userId); + if (!userDataKey) return new Map(); + + const byId = new Map(); + for (const row of rows) { + byId.set( + row.id, + DataCrypto.decryptRecord("ssh_credentials", row, userId, userDataKey), + ); + } + return byId; + } + async findCredentialByIdForOwnerDecryptedAs( credentialId: number, ownerUserId: string, @@ -302,17 +424,32 @@ export class HostResolutionRepository { return this.decryptOne("ssh_credentials", rows[0], decryptUserId); } - async findOverrideCredentialId( - hostId: number, + /** + * Resolve the nearest assigned credential for a folder path, walking up + * through parent folders (e.g. "Switches / Floor1" falls back to + * "Switches" if the child folder has no credential of its own). + */ + async findFolderCredentialId( userId: string, + folderPath: string, ): Promise { - const rows = await this.context.drizzle - .select({ overrideCredentialId: hostAccess.overrideCredentialId }) - .from(hostAccess) - .where(and(eq(hostAccess.hostId, hostId), eq(hostAccess.userId, userId))) - .limit(1); + const segments = folderPath.split(" / ").filter(Boolean); + if (segments.length === 0) return null; - return rows[0]?.overrideCredentialId ?? null; + const paths = segments.map((_, i) => segments.slice(0, i + 1).join(" / ")); + const rows = await this.context.drizzle + .select({ name: sshFolders.name, credentialId: sshFolders.credentialId }) + .from(sshFolders) + .where( + and(eq(sshFolders.userId, userId), inArray(sshFolders.name, paths)), + ); + + const byName = new Map(rows.map((row) => [row.name, row.credentialId])); + for (let i = paths.length - 1; i >= 0; i--) { + const credentialId = byName.get(paths[i]); + if (credentialId) return credentialId; + } + return null; } private decryptOne>( diff --git a/src/backend/database/repositories/host-sidebar-preference-repository.ts b/src/backend/database/repositories/host-sidebar-preference-repository.ts new file mode 100644 index 0000000..e433ea7 --- /dev/null +++ b/src/backend/database/repositories/host-sidebar-preference-repository.ts @@ -0,0 +1,71 @@ +import { eq } from "drizzle-orm"; +import { hostSidebarPreferences } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturningWhere, updateReturning } from "./returning.js"; + +export type HostSidebarPreferenceRecord = + typeof hostSidebarPreferences.$inferSelect; + +export class HostSidebarPreferenceRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async findByUserId( + userId: string, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(hostSidebarPreferences) + .where(eq(hostSidebarPreferences.userId, userId)) + .limit(1); + + return rows[0] ?? null; + } + + async upsert( + userId: string, + data: string, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findByUserId(userId); + + if (!existing) { + const rows = await insertReturningWhere( + this.context, + hostSidebarPreferences, + { userId, data, updatedAt: now }, + eq(hostSidebarPreferences.userId, userId), + ); + await this.afterWrite(); + return rows[0]; + } + + const rows = await updateReturning( + this.context, + hostSidebarPreferences, + { data, updatedAt: now }, + eq(hostSidebarPreferences.userId, userId), + ); + await this.afterWrite(); + return rows[0]; + } + + async deleteByUserId(userId: string): Promise { + const result = await this.context.drizzle + .delete(hostSidebarPreferences) + .where(eq(hostSidebarPreferences.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + + return rowsAffected(result); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/mutation-result.ts b/src/backend/database/repositories/mutation-result.ts new file mode 100644 index 0000000..d849c77 --- /dev/null +++ b/src/backend/database/repositories/mutation-result.ts @@ -0,0 +1,157 @@ +import type { DatabaseDialect } from "../db/dialect.js"; + +/** + * Reading the outcome of a write without depending on RETURNING. + * + * SQLite and Postgres can attach `.returning()` to a delete or update and get + * the affected rows back. **MySQL cannot** โ€” it has no RETURNING clause, and + * drizzle's mysql-core does not expose the method at all, so the call is a + * TypeError rather than a bad query. 175 call sites here read a write's result, + * so the difference has to be absorbed somewhere. + * + * The split that matters is what the caller actually needs: + * + * - **How many rows changed** โ€” the majority, and none of them need the rows. + * They used to ask for them anyway, via `.returning().length`. Dropping the + * `.returning()` and reading the driver's own count is both portable and one + * less thing for the database to send back. + * - **The rows themselves** โ€” cannot be emulated on MySQL without reading + * first, which needs a transaction to stay correct under concurrency. Those + * call sites are handled individually rather than behind a helper that hides + * an extra round trip. + */ + +/** + * The count each driver reports for a write, under its own name. + * + * Every engine says how many rows a write touched. None of them agree on what + * to call it: + * + * | driver | shape | + * |----------------|----------------------------------------| + * | better-sqlite3 | `{ changes, lastInsertRowid }` | + * | node-postgres | `{ rowCount, rows, command }` | + * | mysql2 | `[{ affectedRows, insertId }, fields]` | + * + * These are the shapes returned when NO `.returning()` is attached โ€” which is + * the portable way to write, since MySQL has no RETURNING clause at all. + */ +interface WriteHeader { + changes?: number; + rowCount?: number; + affectedRows?: number; + lastInsertRowid?: number | bigint; + insertId?: number; +} + +const COUNT_FIELDS = ["changes", "rowCount", "affectedRows"] as const; + +/** + * mysql2 hands back `[ResultSetHeader, fields]`, which is itself an array โ€” so + * "is it an array" cannot distinguish a write header from a returning() result. + * The header is identified by carrying one of the fields above instead. + */ +function asWriteHeader(result: unknown): WriteHeader | null { + const candidate = + Array.isArray(result) && result.length > 0 ? result[0] : result; + + if (!candidate || typeof candidate !== "object") return null; + const header = candidate as WriteHeader; + + const known = + COUNT_FIELDS.some((field) => typeof header[field] === "number") || + typeof header.insertId === "number" || + typeof header.lastInsertRowid === "number" || + typeof header.lastInsertRowid === "bigint"; + + return known ? header : null; +} + +/** + * Number of rows a write touched. + * + * Pass the result of the write itself โ€” every driver's header is understood, so + * the caller neither branches on the dialect nor attaches `.returning()` just to + * count what came back. + * + * A `.returning()` array is still accepted, for the call sites that need the + * rows for their own reasons and would rather not count them twice. + */ +export function rowsAffected(result: unknown): number { + const header = asWriteHeader(result); + if (header) { + for (const field of COUNT_FIELDS) { + const count = header[field]; + if (typeof count === "number") return count; + } + // A header with only insertId: one row went in. + return 0; + } + + if (Array.isArray(result)) return result.length; + return 0; +} + +/** + * Id assigned by an insert. + * + * **Only meaningful on the result of an insert.** SQLite's `lastInsertRowid` and + * MySQL's `insertId` are connection-level values that survive the statement that + * set them โ€” after a delete, SQLite still reports whatever the last insert + * produced. Passing an update or delete result here gets a stale id, not null. + * + * Returns null when the table has no autoincrement key. + */ +export function insertedId(result: unknown): number | null { + const header = asWriteHeader(result); + if (header) { + // MySQL and SQLite both use 0 for "no autoincrement column". + if (typeof header.insertId === "number") { + return header.insertId > 0 ? header.insertId : null; + } + if (typeof header.lastInsertRowid === "bigint") { + return header.lastInsertRowid > 0n + ? Number(header.lastInsertRowid) + : null; + } + if (typeof header.lastInsertRowid === "number") { + return header.lastInsertRowid > 0 ? header.lastInsertRowid : null; + } + return null; + } + + if (Array.isArray(result)) { + const first = result[0] as { id?: unknown } | undefined; + return typeof first?.id === "number" ? first.id : null; + } + + return null; +} + +/** + * Whether `.returning()` can be attached to a write on this engine. + * + * Call sites that genuinely need the affected rows use this to choose between + * one statement and a read-then-write inside a transaction. + */ +export function supportsReturning(dialect: DatabaseDialect): boolean { + return dialect !== "mysql"; +} + +/** + * Reads an aggregate count as a number. + * + * `sql` is a type assertion, not a conversion. Postgres returns COUNT() + * as bigint, which node-postgres hands back as a **string** so that values past + * 2^53 survive โ€” so the annotation is a lie there and comparisons like + * `count < max` compare a string to a number. + */ +export function countValue(value: unknown): number { + if (typeof value === "number") return value; + if (typeof value === "bigint") return Number(value); + if (typeof value === "string") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; + } + return 0; +} diff --git a/src/backend/database/repositories/network-topology-repository.ts b/src/backend/database/repositories/network-topology-repository.ts index fa8024f..074c442 100644 --- a/src/backend/database/repositories/network-topology-repository.ts +++ b/src/backend/database/repositories/network-topology-repository.ts @@ -1,6 +1,7 @@ import { eq } from "drizzle-orm"; import { networkTopology } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; export type NetworkTopologyRecord = typeof networkTopology.$inferSelect; @@ -45,16 +46,15 @@ export class NetworkTopologyRepository { } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(networkTopology) - .where(eq(networkTopology.userId, userId)) - .returning({ id: networkTopology.id }); + .where(eq(networkTopology.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/open-tab-repository.ts b/src/backend/database/repositories/open-tab-repository.ts index 5cad21d..46dc7cd 100644 --- a/src/backend/database/repositories/open-tab-repository.ts +++ b/src/backend/database/repositories/open-tab-repository.ts @@ -1,11 +1,12 @@ import { and, eq, gt } from "drizzle-orm"; import { userOpenTabs } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; export type OpenTabRecord = typeof userOpenTabs.$inferSelect; export type NewOpenTabRecord = typeof userOpenTabs.$inferInsert; export type OpenTabUpdate = Partial< - Pick + Pick >; export type OpenTabUpsertInput = Pick< @@ -111,43 +112,40 @@ export class OpenTabRepository { update: OpenTabUpdate, updatedAt = new Date().toISOString(), ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(userOpenTabs) .set({ ...update, updatedAt }) - .where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId))) - .returning({ id: userOpenTabs.id }); + .where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId))); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteForUser(userId: string, id: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(userOpenTabs) - .where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId))) - .returning({ id: userOpenTabs.id }); + .where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId))); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(userOpenTabs) - .where(eq(userOpenTabs.userId, userId)) - .returning({ id: userOpenTabs.id }); + .where(eq(userOpenTabs.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async findByIdForUser( diff --git a/src/backend/database/repositories/opkssh-token-repository.ts b/src/backend/database/repositories/opkssh-token-repository.ts index 0c15bdd..e802ef8 100644 --- a/src/backend/database/repositories/opkssh-token-repository.ts +++ b/src/backend/database/repositories/opkssh-token-repository.ts @@ -1,6 +1,8 @@ import { and, eq } from "drizzle-orm"; import { opksshTokens } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { upsert } from "./returning.js"; export type OpksshTokenRecord = typeof opksshTokens.$inferSelect; @@ -26,9 +28,10 @@ export class OpksshTokenRepository { async upsert(input: OpksshTokenUpsertInput): Promise { const createdAt = input.createdAt ?? new Date().toISOString(); - await this.context.drizzle - .insert(opksshTokens) - .values({ + await upsert( + this.context, + opksshTokens, + { userId: input.userId, hostId: input.hostId, sshCert: input.sshCert, @@ -38,8 +41,8 @@ export class OpksshTokenRepository { issuer: input.issuer, audience: input.audience, expiresAt: input.expiresAt, - }) - .onConflictDoUpdate({ + }, + { target: [opksshTokens.userId, opksshTokens.hostId], set: { sshCert: input.sshCert, @@ -51,7 +54,8 @@ export class OpksshTokenRepository { expiresAt: input.expiresAt, createdAt, }, - }); + }, + ); await this.afterWrite(); } @@ -76,47 +80,44 @@ export class OpksshTokenRepository { hostId: number, lastUsed = new Date().toISOString(), ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(opksshTokens) .set({ lastUsed }) .where( and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), - ) - .returning({ id: opksshTokens.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserAndHost(userId: string, hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(opksshTokens) .where( and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), - ) - .returning({ id: opksshTokens.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(opksshTokens) - .where(eq(opksshTokens.userId, userId)) - .returning({ id: opksshTokens.id }); + .where(eq(opksshTokens.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/proxmox-node-history-repository.ts b/src/backend/database/repositories/proxmox-node-history-repository.ts new file mode 100644 index 0000000..681ccd0 --- /dev/null +++ b/src/backend/database/repositories/proxmox-node-history-repository.ts @@ -0,0 +1,68 @@ +import { and, asc, eq, gte, lt, lte } from "drizzle-orm"; +import { proxmoxNodeHistory } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { sqlTimestampDaysAgo } from "./sql-timestamp.js"; + +export type ProxmoxNodeHistoryRecord = typeof proxmoxNodeHistory.$inferSelect; + +export interface ProxmoxNodeHistoryCreateInput { + hostId: number; + cpuPercent?: number | null; + memPercent?: number | null; + diskPercent?: number | null; + netRxBytes?: number | null; + netTxBytes?: number | null; +} + +export class ProxmoxNodeHistoryRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async create(input: ProxmoxNodeHistoryCreateInput): Promise { + await this.context.drizzle.insert(proxmoxNodeHistory).values({ + hostId: input.hostId, + cpuPercent: input.cpuPercent, + memPercent: input.memPercent, + diskPercent: input.diskPercent, + netRxBytes: input.netRxBytes, + netTxBytes: input.netTxBytes, + }); + + await this.afterWrite(); + } + + async pruneOlderThan(hostId: number, retentionDays: number): Promise { + await this.context.drizzle + .delete(proxmoxNodeHistory) + .where( + and( + eq(proxmoxNodeHistory.hostId, hostId), + lt(proxmoxNodeHistory.ts, sqlTimestampDaysAgo(retentionDays)), + ), + ); + } + + async listRange( + hostId: number, + fromTs: string, + toTs: string, + ): Promise { + return this.context.drizzle + .select() + .from(proxmoxNodeHistory) + .where( + and( + eq(proxmoxNodeHistory.hostId, hostId), + gte(proxmoxNodeHistory.ts, fromTs), + lte(proxmoxNodeHistory.ts, toTs), + ), + ) + .orderBy(asc(proxmoxNodeHistory.ts)); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/rbac-access-repository.ts b/src/backend/database/repositories/rbac-access-repository.ts index 92a2463..cc3f047 100644 --- a/src/backend/database/repositories/rbac-access-repository.ts +++ b/src/backend/database/repositories/rbac-access-repository.ts @@ -9,6 +9,8 @@ import { users, } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; export type RbacAccessTargetType = "user" | "role"; @@ -57,6 +59,7 @@ export interface RbacVisibleSharedSnippet extends RbacSharedSnippet { order: number; createdAt: string; updatedAt: string; + isNote: boolean; } export interface RbacAccessibleSnippet extends RbacVisibleSharedSnippet { @@ -156,7 +159,7 @@ export class RbacAccessRepository { return { id: existing.id, created: false }; } - const result = await this.context.drizzle.insert(hostAccess).values({ + const [created] = await insertReturning(this.context, hostAccess, { hostId: input.hostId, userId: input.targetType === "user" ? input.targetUserId : null, roleId: input.targetType === "role" ? input.targetRoleId : null, @@ -166,7 +169,7 @@ export class RbacAccessRepository { }); await this.afterWrite(); - return { id: Number(result.lastInsertRowid), created: true }; + return { id: created.id, created: true }; } async revokeHostAccess(accessId: number, hostId: number): Promise { @@ -177,16 +180,15 @@ export class RbacAccessRepository { } async deleteHostAccessForHost(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(hostAccess) - .where(eq(hostAccess.hostId, hostId)) - .returning({ id: hostAccess.id }); + .where(eq(hostAccess.hostId, hostId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteHostAccessForHosts(hostIds: number[]): Promise { @@ -194,30 +196,27 @@ export class RbacAccessRepository { return 0; } - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(hostAccess) - .where(inArray(hostAccess.hostId, hostIds)) - .returning({ id: hostAccess.id }); + .where(inArray(hostAccess.hostId, hostIds)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteHostAccessForUserReferences(userId: string): Promise { - const directRows = await this.context.drizzle + const directResult = await this.context.drizzle .delete(hostAccess) - .where(eq(hostAccess.userId, userId)) - .returning({ id: hostAccess.id }); + .where(eq(hostAccess.userId, userId)); - const grantedRows = await this.context.drizzle + const result = await this.context.drizzle .delete(hostAccess) - .where(eq(hostAccess.grantedBy, userId)) - .returning({ id: hostAccess.id }); + .where(eq(hostAccess.grantedBy, userId)); - const deletedCount = directRows.length + grantedRows.length; + const deletedCount = rowsAffected(directResult) + rowsAffected(result); if (deletedCount > 0) { await this.afterWrite(); } @@ -238,17 +237,6 @@ export class RbacAccessRepository { return rows[0] ?? null; } - async updateHostAccessOverrideCredential( - accessId: number, - credentialId: number | null, - ): Promise { - await this.context.drizzle - .update(hostAccess) - .set({ overrideCredentialId: credentialId }) - .where(eq(hostAccess.id, accessId)); - await this.afterWrite(); - } - async listSnippetAccess(snippetId: number): Promise { const rows = await this.context.drizzle .select({ @@ -291,7 +279,7 @@ export class RbacAccessRepository { return { id: existing.id, created: false }; } - const result = await this.context.drizzle.insert(snippetAccess).values({ + const [created] = await insertReturning(this.context, snippetAccess, { snippetId: input.snippetId, userId: input.targetType === "user" ? input.targetUserId : null, roleId: input.targetType === "role" ? input.targetRoleId : null, @@ -301,7 +289,7 @@ export class RbacAccessRepository { }); await this.afterWrite(); - return { id: Number(result.lastInsertRowid), created: true }; + return { id: created.id, created: true }; } async revokeSnippetAccess( @@ -451,6 +439,7 @@ export class RbacAccessRepository { order: snippets.order, createdAt: snippets.createdAt, updatedAt: snippets.updatedAt, + isNote: snippets.isNote, ownerUsername: users.username, permissionLevel: snippetAccess.permissionLevel, expiresAt: snippetAccess.expiresAt, @@ -487,6 +476,7 @@ export class RbacAccessRepository { createdAt: snippets.createdAt, updatedAt: snippets.updatedAt, hostFilter: snippets.hostFilter, + isNote: snippets.isNote, ownerUsername: users.username, permissionLevel: snippetAccess.permissionLevel, expiresAt: snippetAccess.expiresAt, @@ -512,21 +502,20 @@ export class RbacAccessRepository { async deleteExpiredHostAccess( now = new Date().toISOString(), ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(hostAccess) .where( and( sql`${hostAccess.expiresAt} IS NOT NULL`, sql`${hostAccess.expiresAt} <= ${now}`, ), - ) - .returning({ id: hostAccess.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async findActiveHostAccess( @@ -635,17 +624,16 @@ export class RbacAccessRepository { hostId: number, update: { permissionLevel?: string; expiresAt?: string | null }, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(hostAccess) .set(update) - .where(and(eq(hostAccess.id, accessId), eq(hostAccess.hostId, hostId))) - .returning({ id: hostAccess.id }); + .where(and(eq(hostAccess.id, accessId), eq(hostAccess.hostId, hostId))); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async findHostAccessOwnerId(hostAccessId: number): Promise { diff --git a/src/backend/database/repositories/recent-activity-repository.ts b/src/backend/database/repositories/recent-activity-repository.ts index bd7979f..30b5fd0 100644 --- a/src/backend/database/repositories/recent-activity-repository.ts +++ b/src/backend/database/repositories/recent-activity-repository.ts @@ -1,6 +1,8 @@ import { desc, eq, inArray } from "drizzle-orm"; import { recentActivity } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; export type RecentActivityRecord = typeof recentActivity.$inferSelect; export type NewRecentActivityRecord = typeof recentActivity.$inferInsert; @@ -26,10 +28,7 @@ export class RecentActivityRepository { async create( activity: NewRecentActivityRecord, ): Promise { - const rows = await this.context.drizzle - .insert(recentActivity) - .values(activity) - .returning(); + const rows = await insertReturning(this.context, recentActivity, activity); await this.afterWrite(); return rows[0]; @@ -51,42 +50,39 @@ export class RecentActivityRepository { return 0; } - const deletedRows = await this.context.drizzle + const result = await this.context.drizzle .delete(recentActivity) - .where(inArray(recentActivity.id, idsToDelete)) - .returning({ id: recentActivity.id }); + .where(inArray(recentActivity.id, idsToDelete)); - if (deletedRows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return deletedRows.length; + return rowsAffected(result); } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(recentActivity) - .where(eq(recentActivity.userId, userId)) - .returning({ id: recentActivity.id }); + .where(eq(recentActivity.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(recentActivity) - .where(eq(recentActivity.hostId, hostId)) - .returning({ id: recentActivity.id }); + .where(eq(recentActivity.hostId, hostId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostIds(hostIds: number[]): Promise { @@ -94,16 +90,15 @@ export class RecentActivityRepository { return 0; } - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(recentActivity) - .where(inArray(recentActivity.hostId, hostIds)) - .returning({ id: recentActivity.id }); + .where(inArray(recentActivity.hostId, hostIds)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/returning.ts b/src/backend/database/repositories/returning.ts new file mode 100644 index 0000000..fe2e05b --- /dev/null +++ b/src/backend/database/repositories/returning.ts @@ -0,0 +1,227 @@ +import { eq, type SQL } from "drizzle-orm"; +import type { SQLiteColumn, SQLiteTable } from "drizzle-orm/sqlite-core"; +import type { DatabaseContext } from "./database-context.js"; +import { + insertedId, + rowsAffected, + supportsReturning, +} from "./mutation-result.js"; + +/** + * Writes that need the affected rows back. + * + * `mutation-result.ts` covers the call sites that only wanted a count. These are + * the ones that genuinely read the rows โ€” an updated record to return to the + * caller, a deleted row's fields to clean up alongside it. + * + * SQLite and Postgres do this in one statement with RETURNING. MySQL has no + * such clause, so the read is a second statement, and the pair has to be atomic: + * + * - **insert** โ€” write, then read the row back by its key. + * - **update** โ€” write, then read. Reading first would return the old values. + * - **delete** โ€” read, then write. Reading after would return nothing. + * + * Both run in a transaction. Without one, a concurrent write between the two + * statements makes the returned rows describe a state that never existed, and + * with a connection pool the second statement might not even reach the same + * connection. + * + * ## The trap, and why it cannot bite silently + * + * On MySQL the update path re-reads using the same `where`. If the update + * changes a column that `where` tests, the read finds nothing โ€” SQLite would + * have returned the row. Every current caller filters on an id it does not + * modify, but that is a convention, not a guarantee, so the mismatch is + * detected and thrown rather than returned as an empty array. Same for an + * insert whose row cannot be read back. + * + * Row types come from the table, so call sites keep the typing they had with + * `.returning()` and nothing has to be annotated by hand. + */ + +/** + * What `.set()` accepts: a column's own type, or a SQL expression in its place โ€” + * `updatedAt: sql`CURRENT_TIMESTAMP`` is the common one here. + */ +type UpdateValues = { + [K in keyof T["$inferInsert"]]?: T["$inferInsert"][K] | SQL; +}; + +export async function updateReturning( + context: DatabaseContext, + table: T, + values: UpdateValues, + where: SQL, +): Promise { + const db = context.drizzle; + + if (supportsReturning(context.dialect)) { + // The cast resolves a conditional in drizzle's return type that TypeScript + // cannot narrow while T is still generic. The runtime shape is the rows. + return db.update(table).set(values).where(where).returning() as Promise< + T["$inferSelect"][] + >; + } + + return db.transaction(async (tx) => { + const written = await tx.update(table).set(values).where(where); + const rows = await tx.select().from(table).where(where); + + // The trap this catches: if the update changed a column that `where` tests, + // the read finds nothing and the caller gets [] โ€” on MySQL only, with no + // error, where SQLite would have returned the row. Rows changed but none + // readable back is exactly that case, so make it loud instead. + if (rows.length === 0 && rowsAffected(written) > 0) { + throw new Error( + `updateReturning wrote ${rowsAffected(written)} row(s) but could not read ` + + `them back: the update changed a column the where clause filters on. ` + + `Read the rows first, or filter on a column the update leaves alone.`, + ); + } + + return rows; + }); +} + +export async function deleteReturning( + context: DatabaseContext, + table: T, + where: SQL, +): Promise { + const db = context.drizzle; + + if (supportsReturning(context.dialect)) { + return db.delete(table).where(where).returning() as Promise< + T["$inferSelect"][] + >; + } + + return db.transaction(async (tx) => { + const rows = await tx.select().from(table).where(where); + await tx.delete(table).where(where); + return rows; + }); +} + +/** A table this can read a single row back from. */ +type Keyed = SQLiteTable & { id: SQLiteColumn }; + +/** + * Inserts one row and returns it as stored, including whatever the database + * filled in โ€” defaults, an autoincrement id, a CURRENT_TIMESTAMP. + * + * This is the one case Postgres cannot shortcut either: without RETURNING there + * is no id to read back by. Hence the split is genuinely three-way โ€” except + * that sqlite and pg both have RETURNING, so it collapses to two again. + * + * On MySQL the key comes from one of two places: + * + * - the caller supplied it (tables keyed by a text id, like `users`) + * - the engine assigned it, reported as `insertId` + * + * Restricted to tables with an `id` column, so a table keyed some other way is + * a compile error here rather than a row that silently fails to come back. + */ +export async function insertReturning( + context: DatabaseContext, + table: T, + values: T["$inferInsert"], +): Promise { + const db = context.drizzle; + + if (supportsReturning(context.dialect)) { + return db.insert(table).values(values).returning() as Promise< + T["$inferSelect"][] + >; + } + + return db.transaction(async (tx) => { + const result = await tx.insert(table).values(values); + + const supplied = (values as { id?: string | number }).id; + const key = supplied ?? insertedId(result); + if (key === null || key === undefined) { + throw new Error( + `Insert into ${String(table)} returned no id to read the row back by.`, + ); + } + + const rows = await tx.select().from(table).where(eq(table.id, key)); + if (rows.length === 0) { + throw new Error( + `Inserted into ${String(table)} but could not read the row back by id ${key}.`, + ); + } + return rows; + }); +} + +/** + * Inserts one row into a table keyed by something other than `id`, reading it + * back by an explicit condition. + * + * `user_preferences` is keyed by `userId` and has no `id` column at all, so + * there is no insertId to read back by โ€” the caller has to say what identifies + * the row it just wrote. + */ +export async function insertReturningWhere( + context: DatabaseContext, + table: T, + values: T["$inferInsert"], + where: SQL, +): Promise { + const db = context.drizzle; + + if (supportsReturning(context.dialect)) { + return db.insert(table).values(values).returning() as Promise< + T["$inferSelect"][] + >; + } + + return db.transaction(async (tx) => { + await tx.insert(table).values(values); + const rows = await tx.select().from(table).where(where); + if (rows.length === 0) { + throw new Error( + `Inserted into ${String(table)} but the read-back condition matched nothing.`, + ); + } + return rows; + }); +} + +/** + * Insert, or update the row that collides with it. + * + * The clause has three spellings. SQLite and Postgres take + * `ON CONFLICT (cols) DO UPDATE`; **MySQL takes `ON DUPLICATE KEY UPDATE` and + * names no columns** โ€” it uses whichever unique key was violated. drizzle + * follows suit, so `onConflictDoUpdate` does not exist on mysql-core at all and + * calling it is a TypeError rather than a rejected query. + * + * The conflict target still has to be passed: it is what SQLite and Postgres + * need, and stating it keeps the caller honest about which unique constraint it + * is relying on โ€” four of those were missing from the schema entirely until the + * cross-dialect tests went looking. + */ +export async function upsert( + context: DatabaseContext, + table: T, + values: T["$inferInsert"], + conflict: { target: SQLiteColumn[]; set: UpdateValues }, +): Promise { + const db = context.drizzle; + + if (context.dialect === "mysql") { + const insert = db.insert(table).values(values) as unknown as { + onDuplicateKeyUpdate: (config: { set: UpdateValues }) => Promise; + }; + await insert.onDuplicateKeyUpdate({ set: conflict.set }); + return; + } + + await db + .insert(table) + .values(values) + .onConflictDoUpdate({ target: conflict.target, set: conflict.set }); +} diff --git a/src/backend/database/repositories/role-repository.ts b/src/backend/database/repositories/role-repository.ts index 47f53d7..3790f23 100644 --- a/src/backend/database/repositories/role-repository.ts +++ b/src/backend/database/repositories/role-repository.ts @@ -1,6 +1,8 @@ import { and, eq, inArray } from "drizzle-orm"; import { hostAccess, roles, userRoles } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { deleteReturning, insertReturning } from "./returning.js"; export type RoleRecord = typeof roles.$inferSelect; export type NewRoleRecord = typeof roles.$inferInsert; @@ -62,27 +64,27 @@ export class RoleRepository { } async createRole(role: NewRoleRecord): Promise { - const result = await this.context.drizzle.insert(roles).values(role); + const [created] = await insertReturning(this.context, roles, role); await this.afterWrite(); - return Number(result.lastInsertRowid); + return created.id; } async updateRole(id: number, update: RoleUpdate): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(roles) .set(update) - .where(eq(roles.id, id)) - .returning({ id: roles.id }); + .where(eq(roles.id, id)); await this.afterWrite(); - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteRole(id: number): Promise<{ deletedUserIds: string[] }> { - const deletedUserRoles = await this.context.drizzle - .delete(userRoles) - .where(eq(userRoles.roleId, id)) - .returning({ userId: userRoles.userId }); + const deletedUserRoles = await deleteReturning( + this.context, + userRoles, + eq(userRoles.roleId, id), + ); await this.context.drizzle .delete(hostAccess) @@ -169,16 +171,15 @@ export class RoleRepository { } if (removeRole) { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(userRoles) .where( and( eq(userRoles.userId, input.userId), eq(userRoles.roleId, removeRole.id), ), - ) - .returning({ id: userRoles.id }); - removed = rows.length > 0; + ); + removed = rowsAffected(result) > 0; } if (added || removed) { @@ -196,16 +197,15 @@ export class RoleRepository { } async removeAllRolesFromUser(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(userRoles) - .where(eq(userRoles.userId, userId)) - .returning({ id: userRoles.id }); + .where(eq(userRoles.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async listUserRoleIds(userId: string): Promise { diff --git a/src/backend/database/repositories/session-recording-repository.ts b/src/backend/database/repositories/session-recording-repository.ts index 678a24a..7961e47 100644 --- a/src/backend/database/repositories/session-recording-repository.ts +++ b/src/backend/database/repositories/session-recording-repository.ts @@ -1,6 +1,8 @@ import { and, desc, eq, inArray, lt } from "drizzle-orm"; import { hosts, sessionRecordings } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; export type SessionRecordingRecord = typeof sessionRecordings.$inferSelect; @@ -47,10 +49,11 @@ export class SessionRecordingRepository { async create( input: SessionRecordingCreateInput, ): Promise { - const [created] = await this.context.drizzle - .insert(sessionRecordings) - .values(input) - .returning(); + const [created] = await insertReturning( + this.context, + sessionRecordings, + input, + ); await this.afterWrite(); return created; @@ -58,7 +61,12 @@ export class SessionRecordingRepository { async updateEnded( id: number, - input: { endedAt: string; duration: number | null }, + input: { + endedAt: string; + duration: number | null; + terminatedByOwner?: boolean; + terminationReason?: string; + }, ): Promise { await this.context.drizzle .update(sessionRecordings) @@ -165,57 +173,71 @@ export class SessionRecordingRepository { } async deleteById(id: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sessionRecordings) - .where(eq(sessionRecordings.id, id)) - .returning({ id: sessionRecordings.id }); + .where(eq(sessionRecordings.id, id)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteForUser(userId: string, id: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sessionRecordings) .where( and(eq(sessionRecordings.id, id), eq(sessionRecordings.userId, userId)), - ) - .returning({ id: sessionRecordings.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; + } + + /** + * Detaches recordings from a user being deleted instead of removing them. + * A recording is evidence about the host as much as about the person, and the + * file stays on disk regardless โ€” deleting only the row would orphan it. + */ + async anonymizeByUserId(userId: string): Promise { + const result = await this.context.drizzle + .update(sessionRecordings) + .set({ userId: null }) + .where(eq(sessionRecordings.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + + return rowsAffected(result); } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sessionRecordings) - .where(eq(sessionRecordings.userId, userId)) - .returning({ id: sessionRecordings.id }); + .where(eq(sessionRecordings.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sessionRecordings) - .where(eq(sessionRecordings.hostId, hostId)) - .returning({ id: sessionRecordings.id }); + .where(eq(sessionRecordings.hostId, hostId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostIds(hostIds: number[]): Promise { @@ -223,16 +245,15 @@ export class SessionRecordingRepository { return 0; } - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sessionRecordings) - .where(inArray(sessionRecordings.hostId, hostIds)) - .returning({ id: sessionRecordings.id }); + .where(inArray(sessionRecordings.hostId, hostIds)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/session-repository.ts b/src/backend/database/repositories/session-repository.ts index ff83c46..2d1785e 100644 --- a/src/backend/database/repositories/session-repository.ts +++ b/src/backend/database/repositories/session-repository.ts @@ -1,10 +1,14 @@ import { and, eq, lte, ne } from "drizzle-orm"; import { sessions } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; export type SessionRecord = typeof sessions.$inferSelect; export type NewSessionRecord = typeof sessions.$inferInsert; +const SESSION_ACTIVITY_PERSIST_INTERVAL_MS = 60_000; + export class SessionRepository { constructor( private readonly context: DatabaseContext, @@ -12,10 +16,7 @@ export class SessionRepository { ) {} async create(session: NewSessionRecord): Promise { - const rows = await this.context.drizzle - .insert(sessions) - .values(session) - .returning(); + const rows = await insertReturning(this.context, sessions, session); await this.afterWrite(); return rows[0]; } @@ -51,12 +52,19 @@ export class SessionRepository { async touch( id: string, lastActiveAt = new Date().toISOString(), - ): Promise { - await this.context.drizzle + minIntervalMs = SESSION_ACTIVITY_PERSIST_INTERVAL_MS, + ): Promise { + const cutoff = new Date( + new Date(lastActiveAt).getTime() - minIntervalMs, + ).toISOString(); + const result = await this.context.drizzle .update(sessions) .set({ lastActiveAt }) - .where(eq(sessions.id, id)); + .where(and(eq(sessions.id, id), lte(sessions.lastActiveAt, cutoff))); + + if (rowsAffected(result) === 0) return false; await this.afterWrite(); + return true; } async updateToken( @@ -72,13 +80,12 @@ export class SessionRepository { } async revoke(id: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sessions) - .where(eq(sessions.id, id)) - .returning({ id: sessions.id }); + .where(eq(sessions.id, id)); await this.afterWrite(); - return rows.length > 0; + return rowsAffected(result) > 0; } async revokeAllForUser( @@ -89,23 +96,19 @@ export class SessionRepository { ? and(eq(sessions.userId, userId), ne(sessions.id, exceptSessionId)) : eq(sessions.userId, userId); - const rows = await this.context.drizzle - .delete(sessions) - .where(where) - .returning({ id: sessions.id }); + const result = await this.context.drizzle.delete(sessions).where(where); await this.afterWrite(); - return rows.length; + return rowsAffected(result); } async deleteExpired(now = new Date()): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sessions) - .where(lte(sessions.expiresAt, now.toISOString())) - .returning({ id: sessions.id }); + .where(lte(sessions.expiresAt, now.toISOString())); await this.afterWrite(); - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/session-share-repository.ts b/src/backend/database/repositories/session-share-repository.ts new file mode 100644 index 0000000..39a8495 --- /dev/null +++ b/src/backend/database/repositories/session-share-repository.ts @@ -0,0 +1,243 @@ +import { and, eq, gt, isNull, lt } from "drizzle-orm"; +import { + hosts, + sessionShareParticipants, + sessionShares, + users, +} from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; + +export type SessionShareRecord = typeof sessionShares.$inferSelect; +export type SessionShareParticipantRecord = + typeof sessionShareParticipants.$inferSelect; + +export type SessionShareType = "link" | "user"; +export type SessionSharePermissionLevel = "read-only" | "read-write"; + +export interface SessionShareCreateInput { + id: string; + hostId: number; + ownerUserId: string; + protocol: string; + sessionId: string; + tabInstanceId?: string | null; + shareType: SessionShareType; + targetUserId?: string | null; + linkToken?: string | null; + permissionLevel: SessionSharePermissionLevel; + expiresAt: string; +} + +export interface SessionShareWithHost extends SessionShareRecord { + hostName: string | null; + ownerUsername: string | null; +} + +export interface SharedWithMeRecord extends SessionShareRecord { + hostName: string | null; + ownerUsername: string | null; +} + +function activeShareFilter(now: string) { + return and(isNull(sessionShares.revokedAt), gt(sessionShares.expiresAt, now)); +} + +export class SessionShareRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async create(input: SessionShareCreateInput): Promise { + const [created] = await insertReturning(this.context, sessionShares, { + id: input.id, + hostId: input.hostId, + ownerUserId: input.ownerUserId, + protocol: input.protocol, + sessionId: input.sessionId, + tabInstanceId: input.tabInstanceId ?? null, + shareType: input.shareType, + targetUserId: input.targetUserId ?? null, + linkToken: input.linkToken ?? null, + permissionLevel: input.permissionLevel, + expiresAt: input.expiresAt, + }); + + await this.afterWrite(); + return created; + } + + async findById(id: string): Promise { + const rows = await this.context.drizzle + .select() + .from(sessionShares) + .where(eq(sessionShares.id, id)) + .limit(1); + return rows[0] ?? null; + } + + async findActiveById( + id: string, + now = new Date().toISOString(), + ): Promise { + const rows = await this.context.drizzle + .select() + .from(sessionShares) + .where(and(eq(sessionShares.id, id), activeShareFilter(now))) + .limit(1); + return rows[0] ?? null; + } + + async findByLinkToken( + linkToken: string, + now = new Date().toISOString(), + ): Promise { + const rows = await this.context.drizzle + .select() + .from(sessionShares) + .where( + and(eq(sessionShares.linkToken, linkToken), activeShareFilter(now)), + ) + .limit(1); + return rows[0] ?? null; + } + + async findActiveSharesForHost( + hostId: number, + ownerUserId: string, + now = new Date().toISOString(), + ): Promise { + return this.context.drizzle + .select() + .from(sessionShares) + .where( + and( + eq(sessionShares.hostId, hostId), + eq(sessionShares.ownerUserId, ownerUserId), + activeShareFilter(now), + ), + ); + } + + async findSharesTargetingUser( + userId: string, + now = new Date().toISOString(), + ): Promise { + const rows = await this.context.drizzle + .select({ + share: sessionShares, + hostName: hosts.name, + ownerUsername: users.username, + }) + .from(sessionShares) + .leftJoin(hosts, eq(sessionShares.hostId, hosts.id)) + .leftJoin(users, eq(sessionShares.ownerUserId, users.id)) + .where( + and( + eq(sessionShares.shareType, "user"), + eq(sessionShares.targetUserId, userId), + activeShareFilter(now), + ), + ); + + return rows.map((row) => ({ + ...row.share, + hostName: row.hostName, + ownerUsername: row.ownerUsername, + })); + } + + async revoke(shareId: string, requestingUserId: string): Promise { + const result = await this.context.drizzle + .update(sessionShares) + .set({ revokedAt: new Date().toISOString() }) + .where( + and( + eq(sessionShares.id, shareId), + eq(sessionShares.ownerUserId, requestingUserId), + ), + ); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + return rowsAffected(result) > 0; + } + + async revokeAsAdmin(shareId: string): Promise { + const result = await this.context.drizzle + .update(sessionShares) + .set({ revokedAt: new Date().toISOString() }) + .where(eq(sessionShares.id, shareId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + return rowsAffected(result) > 0; + } + + async deleteExpiredShares(now = new Date().toISOString()): Promise { + const result = await this.context.drizzle + .delete(sessionShares) + .where(lt(sessionShares.expiresAt, now)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + return rowsAffected(result); + } + + async touchShareUsage( + shareId: string, + lastJoinedAt = new Date().toISOString(), + ): Promise { + const current = await this.findById(shareId); + await this.context.drizzle + .update(sessionShares) + .set({ + lastJoinedAt, + joinCount: (current?.joinCount ?? 0) + 1, + }) + .where(eq(sessionShares.id, shareId)); + await this.afterWrite(); + } + + async recordParticipantJoin( + shareId: string, + userId: string | null, + guestLabel: string | null, + ): Promise { + const [created] = await insertReturning( + this.context, + sessionShareParticipants, + { shareId, userId, guestLabel }, + ); + await this.afterWrite(); + return created; + } + + async recordParticipantLeave(participantId: number): Promise { + await this.context.drizzle + .update(sessionShareParticipants) + .set({ leftAt: new Date().toISOString() }) + .where(eq(sessionShareParticipants.id, participantId)); + await this.afterWrite(); + } + + async deleteSharesForHost(hostId: number): Promise { + const result = await this.context.drizzle + .delete(sessionShares) + .where(eq(sessionShares.hostId, hostId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + return rowsAffected(result); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/settings-cache.ts b/src/backend/database/repositories/settings-cache.ts new file mode 100644 index 0000000..a081c38 --- /dev/null +++ b/src/backend/database/repositories/settings-cache.ts @@ -0,0 +1,58 @@ +/** + * Synchronous read-through cache for the settings table. + * + * 27 call sites read settings synchronously โ€” during startup, inside request + * handlers, and from the guacd server bootstrap. On SQLite that works because + * better-sqlite3 is synchronous; on Postgres or MySQL there is no synchronous + * query at all, and making all 27 async would push `await` through code paths + * that have no business being asynchronous. + * + * Settings are a handful of low-cardinality configuration rows that change + * rarely and are read constantly, so they are cached in full. Writes go through + * SettingsRepository, which updates the cache in the same call, and the cache is + * primed once at startup. + * + * Known limitation with more than one instance: a write only updates the cache + * of the process that made it. Other instances keep serving the old value until + * their own refresh comes round (SETTINGS_CACHE_REFRESH_SECONDS, 30s default), + * so a settings change takes up to that long to apply fleet-wide. Fixing it + * properly needs cross-instance invalidation, which is not in place yet. + */ + +let cache: Map | null = null; + +export function isSettingsCachePrimed(): boolean { + return cache !== null; +} + +/** Loads the full settings table. Called once during startup. */ +export function primeSettingsCache( + rows: { key: string; value: string }[], +): void { + cache = new Map(rows.map((row) => [row.key, row.value])); +} + +/** + * Reads a cached setting. + * + * Returns null both for "not set" and "cache not primed yet" โ€” every caller + * already treats a missing setting as "use the default", and startup ordering + * means a read before priming should behave the same way rather than throw. + */ +export function readCachedSetting(key: string): string | null { + return cache?.get(key) ?? null; +} + +/** Keeps the cache in step with a write. */ +export function updateCachedSetting(key: string, value: string): void { + cache?.set(key, value); +} + +export function forgetCachedSetting(key: string): void { + cache?.delete(key); +} + +/** Test seam. */ +export function resetSettingsCache(): void { + cache = null; +} diff --git a/src/backend/database/repositories/settings-repository.ts b/src/backend/database/repositories/settings-repository.ts index 3b69fbb..887e432 100644 --- a/src/backend/database/repositories/settings-repository.ts +++ b/src/backend/database/repositories/settings-repository.ts @@ -1,6 +1,8 @@ import { eq, like } from "drizzle-orm"; import { settings } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { forgetCachedSetting, updateCachedSetting } from "./settings-cache.js"; +import { deleteReturning } from "./returning.js"; export class SettingsRepository { constructor( @@ -34,6 +36,9 @@ export class SettingsRepository { const existing = await this.get(key); if (existing === null) { await this.context.drizzle.insert(settings).values({ key, value }); + // Kept in step here so the synchronous readers cannot observe a stale + // value after a write in the same process. + updateCachedSetting(key, value); await this.afterWrite(); return; } @@ -42,23 +47,69 @@ export class SettingsRepository { .update(settings) .set({ value }) .where(eq(settings.key, key)); + updateCachedSetting(key, value); await this.afterWrite(); } + async setMany(entries: Array<{ key: string; value: string }>): Promise { + if (this.context.dialect !== "sqlite") { + await this.context.drizzle.transaction(async (tx) => { + for (const { key, value } of entries) { + const existing = await tx + .select({ value: settings.value }) + .from(settings) + .where(eq(settings.key, key)) + .limit(1); + if (existing[0] === undefined) { + await tx.insert(settings).values({ key, value }); + } else { + await tx + .update(settings) + .set({ value }) + .where(eq(settings.key, key)); + } + } + }); + } else { + this.context.drizzle.transaction((tx) => { + for (const { key, value } of entries) { + const existing = tx + .select({ value: settings.value }) + .from(settings) + .where(eq(settings.key, key)) + .limit(1) + .all(); + if (existing[0] === undefined) { + tx.insert(settings).values({ key, value }).run(); + } else { + tx.update(settings) + .set({ value }) + .where(eq(settings.key, key)) + .run(); + } + } + }); + } + for (const { key, value } of entries) updateCachedSetting(key, value); + await this.afterWrite(); + } async upsert(key: string, value: string): Promise { await this.set(key, value); } async delete(key: string): Promise { await this.context.drizzle.delete(settings).where(eq(settings.key, key)); + forgetCachedSetting(key); await this.afterWrite(); } async deleteLike(pattern: string): Promise { - const rows = await this.context.drizzle - .delete(settings) - .where(like(settings.key, pattern)) - .returning({ key: settings.key }); + const rows = await deleteReturning( + this.context, + settings, + like(settings.key, pattern), + ); + for (const row of rows) forgetCachedSetting(row.key); await this.afterWrite(); return rows.length; } diff --git a/src/backend/database/repositories/shared-host-auth-override-repository.ts b/src/backend/database/repositories/shared-host-auth-override-repository.ts new file mode 100644 index 0000000..653c50f --- /dev/null +++ b/src/backend/database/repositories/shared-host-auth-override-repository.ts @@ -0,0 +1,102 @@ +import { and, eq } from "drizzle-orm"; +import type { AuthOverrideProtocol } from "../../../types/auth-protocols.js"; +import { sharedHostAuthOverrides } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { deleteReturning, upsert } from "./returning.js"; + +export type SharedHostAuthOverrideRecord = + typeof sharedHostAuthOverrides.$inferSelect; + +export class SharedHostAuthOverrideRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async findForHostUser( + hostId: number, + userId: string, + protocol: AuthOverrideProtocol, + ): Promise { + const rows = await this.context.drizzle + .select() + .from(sharedHostAuthOverrides) + .where( + and( + eq(sharedHostAuthOverrides.hostId, hostId), + eq(sharedHostAuthOverrides.userId, userId), + eq(sharedHostAuthOverrides.protocol, protocol), + ), + ) + .limit(1); + + return rows[0] ?? null; + } + + async findCredentialId( + hostId: number, + userId: string, + protocol: AuthOverrideProtocol, + ): Promise { + return ( + (await this.findForHostUser(hostId, userId, protocol))?.credentialId ?? + null + ); + } + + async setCredential( + hostId: number, + userId: string, + protocol: AuthOverrideProtocol, + credentialId: number, + ): Promise { + await upsert( + this.context, + sharedHostAuthOverrides, + { + hostId, + userId, + protocol, + credentialId, + }, + { + target: [ + sharedHostAuthOverrides.hostId, + sharedHostAuthOverrides.userId, + sharedHostAuthOverrides.protocol, + ], + set: { + credentialId, + updatedAt: new Date().toISOString(), + }, + }, + ); + + await this.afterWrite(); + } + + async clearCredential( + hostId: number, + userId: string, + protocol: AuthOverrideProtocol, + ): Promise { + const rows = await deleteReturning( + this.context, + sharedHostAuthOverrides, + and( + eq(sharedHostAuthOverrides.hostId, hostId), + eq(sharedHostAuthOverrides.userId, userId), + eq(sharedHostAuthOverrides.protocol, protocol), + ), + ); + + if (rows.length > 0) { + await this.afterWrite(); + } + return rows.length > 0; + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/shared-host-secrets-repository.ts b/src/backend/database/repositories/shared-host-secrets-repository.ts index 23e2a86..56cd224 100644 --- a/src/backend/database/repositories/shared-host-secrets-repository.ts +++ b/src/backend/database/repositories/shared-host-secrets-repository.ts @@ -1,6 +1,7 @@ import { and, eq, inArray, or } from "drizzle-orm"; import { hostAccess, hosts, sharedHostSecrets } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; export type SharedHostSecretRecord = typeof sharedHostSecrets.$inferSelect; export type NewSharedHostSecretRecord = typeof sharedHostSecrets.$inferInsert; @@ -108,16 +109,15 @@ export class SharedHostSecretsRepository { } async deleteByHostAccessId(hostAccessId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sharedHostSecrets) - .where(eq(sharedHostSecrets.hostAccessId, hostAccessId)) - .returning({ id: sharedHostSecrets.id }); + .where(eq(sharedHostSecrets.hostAccessId, hostAccessId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteForRoleMember( @@ -148,29 +148,27 @@ export class SharedHostSecretsRepository { } async deleteByOriginalCredentialId(credentialId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sharedHostSecrets) - .where(eq(sharedHostSecrets.originalCredentialId, credentialId)) - .returning({ id: sharedHostSecrets.id }); + .where(eq(sharedHostSecrets.originalCredentialId, credentialId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByTargetUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sharedHostSecrets) - .where(eq(sharedHostSecrets.targetUserId, userId)) - .returning({ id: sharedHostSecrets.id }); + .where(eq(sharedHostSecrets.targetUserId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async findHostIdsReferencingCredential( diff --git a/src/backend/database/repositories/snippet-repository.ts b/src/backend/database/repositories/snippet-repository.ts index 5196ff3..32b3d56 100644 --- a/src/backend/database/repositories/snippet-repository.ts +++ b/src/backend/database/repositories/snippet-repository.ts @@ -1,6 +1,13 @@ import { and, asc, eq, sql } from "drizzle-orm"; +import { randomUUID } from "crypto"; import { snippetFolders, snippets } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; export type SnippetRecord = typeof snippets.$inferSelect; export type SnippetFolderRecord = typeof snippetFolders.$inferSelect; @@ -19,6 +26,7 @@ export interface NewSnippetInput { folder?: string | null; order?: number | null; hostFilter?: unknown; + isNote?: boolean; } export interface SnippetUpdateInput { name?: string; @@ -27,6 +35,7 @@ export interface SnippetUpdateInput { folder?: string | null; order?: number; hostFilter?: unknown; + isNote?: boolean; } export interface UpdateSnippetResult { existing: SnippetRecord; @@ -83,11 +92,16 @@ export class SnippetRepository { } async listSnippetsForExport(userId: string): Promise { - return this.context.drizzle - .select() - .from(snippets) - .where(eq(snippets.userId, userId)) - .orderBy(asc(snippets.folder), asc(snippets.order)); + return ( + this.context.drizzle + .select() + .from(snippets) + .where(eq(snippets.userId, userId)) + // coalesce, not asc(folder): folder is nullable, and NULLs sort first on + // SQLite and MySQL but last on Postgres. An export whose row order depends + // on the engine is not much of an export. + .orderBy(sql`coalesce(${snippets.folder}, '')`, asc(snippets.order)) + ); } async listFoldersForExport(userId: string): Promise { @@ -148,18 +162,17 @@ export class SnippetRepository { ? await this.nextOrderForFolder(userId, folderValue) : input.order; - const rows = await this.context.drizzle - .insert(snippets) - .values({ - userId, - name: input.name.trim(), - content: input.content.trim(), - description: input.description?.trim() || null, - folder: input.folder?.trim() || null, - order, - hostFilter: input.hostFilter ? JSON.stringify(input.hostFilter) : null, - }) - .returning(); + const rows = await insertReturning(this.context, snippets, { + syncId: randomUUID(), + userId, + name: input.name.trim(), + content: input.content.trim(), + description: input.description?.trim() || null, + folder: input.folder?.trim() || null, + order, + hostFilter: input.hostFilter ? JSON.stringify(input.hostFilter) : null, + isNote: input.isNote ?? false, + }); await this.afterWrite(); return rows[0]; @@ -181,6 +194,7 @@ export class SnippetRepository { folder: string | null; order: number; hostFilter: string | null; + isNote: boolean; }> = { updatedAt: sql`CURRENT_TIMESTAMP`, }; @@ -197,12 +211,14 @@ export class SnippetRepository { updateFields.hostFilter = input.hostFilter ? JSON.stringify(input.hostFilter) : null; + if (input.isNote !== undefined) updateFields.isNote = input.isNote; - const rows = await this.context.drizzle - .update(snippets) - .set(updateFields) - .where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId))) - .returning(); + const rows = await updateReturning( + this.context, + snippets, + updateFields, + and(eq(snippets.id, snippetId), eq(snippets.userId, userId)), + ); await this.afterWrite(); return { existing, updated: rows[0] }; @@ -227,23 +243,21 @@ export class SnippetRepository { snippetsDeleted: number; foldersDeleted: number; }> { - const deletedSnippets = await this.context.drizzle + const snippetResult = await this.context.drizzle .delete(snippets) - .where(eq(snippets.userId, userId)) - .returning({ id: snippets.id }); + .where(eq(snippets.userId, userId)); - const deletedFolders = await this.context.drizzle + const result = await this.context.drizzle .delete(snippetFolders) - .where(eq(snippetFolders.userId, userId)) - .returning({ id: snippetFolders.id }); + .where(eq(snippetFolders.userId, userId)); - if (deletedSnippets.length > 0 || deletedFolders.length > 0) { + if (rowsAffected(snippetResult) > 0 || rowsAffected(result) > 0) { await this.afterWrite(); } return { - snippetsDeleted: deletedSnippets.length, - foldersDeleted: deletedFolders.length, + snippetsDeleted: rowsAffected(snippetResult), + foldersDeleted: rowsAffected(result), }; } @@ -343,6 +357,7 @@ export class SnippetRepository { const maxOrder = await this.maxOrderForFolder(userId, folderVal); await this.context.drizzle.insert(snippets).values({ + syncId: randomUUID(), userId, name: snippet.name.trim(), content: snippet.content.trim(), @@ -374,15 +389,13 @@ export class SnippetRepository { const existing = await this.findFolderByName(userId, name); if (existing) return null; - const rows = await this.context.drizzle - .insert(snippetFolders) - .values({ - userId, - name: name.trim(), - color: color?.trim() || null, - icon: icon?.trim() || null, - }) - .returning(); + const rows = await insertReturning(this.context, snippetFolders, { + syncId: randomUUID(), + userId, + name: name.trim(), + color: color?.trim() || null, + icon: icon?.trim() || null, + }); if (triggerSave) { await this.afterWrite(); @@ -410,13 +423,12 @@ export class SnippetRepository { if (color !== undefined) updateFields.color = color?.trim() || null; if (icon !== undefined) updateFields.icon = icon?.trim() || null; - const rows = await this.context.drizzle - .update(snippetFolders) - .set(updateFields) - .where( - and(eq(snippetFolders.userId, userId), eq(snippetFolders.name, name)), - ) - .returning(); + const rows = await updateReturning( + this.context, + snippetFolders, + updateFields, + and(eq(snippetFolders.userId, userId), eq(snippetFolders.name, name)), + ); await this.afterWrite(); return rows[0] ?? null; @@ -452,19 +464,23 @@ export class SnippetRepository { return { status: "renamed" }; } - async deleteFolder(userId: string, name: string): Promise { + async deleteFolder( + userId: string, + name: string, + ): Promise<{ syncId: string | null } | null> { await this.context.drizzle .update(snippets) .set({ folder: null }) .where(and(eq(snippets.userId, userId), eq(snippets.folder, name))); - await this.context.drizzle - .delete(snippetFolders) - .where( - and(eq(snippetFolders.userId, userId), eq(snippetFolders.name, name)), - ); + const rows = await deleteReturning( + this.context, + snippetFolders, + and(eq(snippetFolders.userId, userId), eq(snippetFolders.name, name)), + ); await this.afterWrite(); + return rows[0] ? { syncId: rows[0].syncId } : null; } private async findFolderByName( diff --git a/src/backend/database/repositories/sql-timestamp.ts b/src/backend/database/repositories/sql-timestamp.ts new file mode 100644 index 0000000..3d41168 --- /dev/null +++ b/src/backend/database/repositories/sql-timestamp.ts @@ -0,0 +1,20 @@ +/** + * Timestamp columns are stored as text defaulting to `CURRENT_TIMESTAMP`, which + * every supported engine writes as `YYYY-MM-DD HH:MM:SS` in UTC. That format + * sorts lexicographically in time order, so retention cutoffs can be plain + * string comparisons. + * + * Computing the cutoff here rather than with `datetime('now', ?)` keeps the + * queries free of engine-specific date functions. + */ +export function sqlTimestampDaysAgo( + days: number, + now: Date = new Date(), +): string { + const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); + return formatSqlTimestamp(cutoff); +} + +export function formatSqlTimestamp(date: Date): string { + return date.toISOString().slice(0, 19).replace("T", " "); +} diff --git a/src/backend/database/repositories/sqlite-foreign-keys.ts b/src/backend/database/repositories/sqlite-foreign-keys.ts index 09abf11..8b55d75 100644 --- a/src/backend/database/repositories/sqlite-foreign-keys.ts +++ b/src/backend/database/repositories/sqlite-foreign-keys.ts @@ -1,4 +1,5 @@ import { getCurrentRepositorySqlite } from "./factory.js"; +import { needsExplicitPersist, resolveDatabaseDialect } from "../db/dialect.js"; export interface SqliteForeignKeyClient { exec(sql: string): unknown; @@ -16,8 +17,28 @@ export async function withSqliteForeignKeysDisabled( } } +/** + * Runs a bulk import with foreign keys relaxed. + * + * Backup restore writes tables in an order that is not dependency-safe, so the + * constraints have to stand down for the duration. + * + * **This has no equivalent on Postgres or MySQL here.** Postgres needs + * superuser to disable triggers, and MySQL's `SET FOREIGN_KEY_CHECKS = 0` is + * per-connection, which a pool does not guarantee. Rather than run the import + * with constraints enforced and have it fail partway through โ€” leaving a + * half-restored database โ€” it refuses with a message that says why. + */ export async function withCurrentSqliteForeignKeysDisabled( operation: () => Promise, ): Promise { + const dialect = resolveDatabaseDialect(); + if (!needsExplicitPersist(dialect)) { + throw new Error( + `Importing a backup is only supported on SQLite; this deployment uses ${dialect}. ` + + `Restore into the database directly with its own tooling instead.`, + ); + } + return withSqliteForeignKeysDisabled(getCurrentRepositorySqlite(), operation); } diff --git a/src/backend/database/repositories/ssh-credential-usage-repository.ts b/src/backend/database/repositories/ssh-credential-usage-repository.ts index 97d1aa0..4558d64 100644 --- a/src/backend/database/repositories/ssh-credential-usage-repository.ts +++ b/src/backend/database/repositories/ssh-credential-usage-repository.ts @@ -1,6 +1,8 @@ import { eq, inArray } from "drizzle-orm"; import { sshCredentialUsage } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; export type SshCredentialUsageRecord = typeof sshCredentialUsage.$inferSelect; @@ -22,38 +24,37 @@ export class SshCredentialUsageRepository { hostId: number, userId: string, ): Promise { - const [created] = await this.context.drizzle - .insert(sshCredentialUsage) - .values({ credentialId, hostId, userId }) - .returning(); + const [created] = await insertReturning(this.context, sshCredentialUsage, { + credentialId, + hostId, + userId, + }); await this.afterWrite(); return created; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sshCredentialUsage) - .where(eq(sshCredentialUsage.userId, userId)) - .returning({ id: sshCredentialUsage.id }); + .where(eq(sshCredentialUsage.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sshCredentialUsage) - .where(eq(sshCredentialUsage.hostId, hostId)) - .returning({ id: sshCredentialUsage.id }); + .where(eq(sshCredentialUsage.hostId, hostId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostIds(hostIds: number[]): Promise { @@ -61,16 +62,15 @@ export class SshCredentialUsageRepository { return 0; } - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(sshCredentialUsage) - .where(inArray(sshCredentialUsage.hostId, hostIds)) - .returning({ id: sshCredentialUsage.id }); + .where(inArray(sshCredentialUsage.hostId, hostIds)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/sso-provider-repository.ts b/src/backend/database/repositories/sso-provider-repository.ts index 403a81a..275e398 100644 --- a/src/backend/database/repositories/sso-provider-repository.ts +++ b/src/backend/database/repositories/sso-provider-repository.ts @@ -1,6 +1,8 @@ import { asc, eq } from "drizzle-orm"; import { ssoProviders, users } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; export type SsoProviderRecord = typeof ssoProviders.$inferSelect; export type NewSsoProviderRecord = typeof ssoProviders.$inferInsert; @@ -76,10 +78,7 @@ export class SsoProviderRepository { } async create(provider: NewSsoProviderRecord): Promise { - const rows = await this.context.drizzle - .insert(ssoProviders) - .values(provider) - .returning(); + const rows = await insertReturning(this.context, ssoProviders, provider); await this.afterWrite(); return rows[0]; @@ -89,27 +88,27 @@ export class SsoProviderRepository { id: number, update: SsoProviderUpdate, ): Promise { - const rows = await this.context.drizzle - .update(ssoProviders) - .set(update) - .where(eq(ssoProviders.id, id)) - .returning(); + const rows = await updateReturning( + this.context, + ssoProviders, + update, + eq(ssoProviders.id, id), + ); await this.afterWrite(); return rows[0] ?? null; } async delete(id: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(ssoProviders) - .where(eq(ssoProviders.id, id)) - .returning({ id: ssoProviders.id }); + .where(eq(ssoProviders.id, id)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async countUsersByProviderId(providerId: number): Promise { diff --git a/src/backend/database/repositories/sync-tombstone-repository.ts b/src/backend/database/repositories/sync-tombstone-repository.ts new file mode 100644 index 0000000..a0378aa --- /dev/null +++ b/src/backend/database/repositories/sync-tombstone-repository.ts @@ -0,0 +1,73 @@ +import { and, eq } from "drizzle-orm"; +import { syncTombstones } from "../db/schema.js"; +import { timestampAtOrAfter } from "../sync-timestamp.js"; +import type { DatabaseContext } from "./database-context.js"; + +export type SyncTombstoneRecord = typeof syncTombstones.$inferSelect; + +export type SyncEntityType = + | "hosts" + | "sshCredentials" + | "sshFolders" + | "snippets" + | "snippetFolders" + | "vaultProfiles" + | "dashboardServiceLinks" + | "homepageItems" + | "userPreferences"; + +export class SyncTombstoneRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async record( + userId: string, + entityType: SyncEntityType, + syncId: string, + ): Promise { + if (!syncId) return; + await this.context.drizzle.insert(syncTombstones).values({ + userId, + entityType, + syncId, + }); + await this.afterWrite(); + } + + async recordMany( + userId: string, + entityType: SyncEntityType, + syncIds: string[], + ): Promise { + const rows = syncIds.filter(Boolean); + if (rows.length === 0) return; + await this.context.drizzle + .insert(syncTombstones) + .values(rows.map((syncId) => ({ userId, entityType, syncId }))); + await this.afterWrite(); + } + + async listSince( + userId: string, + entityType: SyncEntityType, + since: string | null, + ): Promise { + const conditions = [ + eq(syncTombstones.userId, userId), + eq(syncTombstones.entityType, entityType), + ]; + if (since) + conditions.push(timestampAtOrAfter(syncTombstones.deletedAt, since)); + + return this.context.drizzle + .select() + .from(syncTombstones) + .where(and(...conditions)); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/termix-identity-ca-repository.ts b/src/backend/database/repositories/termix-identity-ca-repository.ts index ea1fcd3..c95e9ce 100644 --- a/src/backend/database/repositories/termix-identity-ca-repository.ts +++ b/src/backend/database/repositories/termix-identity-ca-repository.ts @@ -2,6 +2,12 @@ import { eq } from "drizzle-orm"; import { termixIdentityCa } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; +import { + insertedId, + rowsAffected, + supportsReturning, +} from "./mutation-result.js"; +import { updateReturning } from "./returning.js"; export type TermixIdentityCaRecord = typeof termixIdentityCa.$inferSelect; export type NewTermixIdentityCaRecord = typeof termixIdentityCa.$inferInsert; @@ -54,27 +60,7 @@ export class TermixIdentityCaRepository { ca: NewTermixIdentityCaRecord, ): Promise { const userDataKey = DataCrypto.validateUserAccess(userId); - const result = this.context.drizzle.transaction((tx) => { - const inserted = tx - .insert(termixIdentityCa) - .values({ ...ca, privateKey: "" }) - .returning() - .all(); - const row = inserted[0]; - const encrypted = DataCrypto.encryptRecord( - "termix_identity_ca", - { id: row.id, privateKey: ca.privateKey }, - userId, - userDataKey, - ); - - return tx - .update(termixIdentityCa) - .set({ privateKey: encrypted.privateKey }) - .where(eq(termixIdentityCa.id, row.id)) - .returning() - .all()[0]; - }); + const result = await this.insertThenEncrypt(userId, ca, userDataKey); await this.afterWrite(); return DataCrypto.decryptRecord( @@ -85,6 +71,81 @@ export class TermixIdentityCaRepository { ); } + /** + * Writes a CA in two steps, because the ciphertext depends on the id. + * + * The private key is encrypted with the row's own id as context, which does + * not exist until the row does. So: insert with an empty key, encrypt, update. + * The empty key must never be observable, hence the transaction. + * + * Two branches because better-sqlite3 rejects an async transaction callback โ€” + * see the same note in UserRepository. + */ + private async insertThenEncrypt( + userId: string, + ca: NewTermixIdentityCaRecord, + userDataKey: Buffer, + ): Promise { + const draft = { ...ca, privateKey: "" }; + + const seal = (id: number) => + DataCrypto.encryptRecord( + "termix_identity_ca", + { id, privateKey: ca.privateKey }, + userId, + userDataKey, + ).privateKey; + + if (this.context.dialect === "sqlite") { + /* eslint-disable no-restricted-syntax -- sqlite-only branch: the dialect + is checked directly above, and better-sqlite3 needs the synchronous + .all() form, which has no async equivalent. */ + return this.context.drizzle.transaction((tx) => { + const row = tx + .insert(termixIdentityCa) + .values(draft) + .returning() + .all()[0]; + return tx + .update(termixIdentityCa) + .set({ privateKey: seal(row.id) }) + .where(eq(termixIdentityCa.id, row.id)) + .returning() + .all()[0]; + }); + /* eslint-enable no-restricted-syntax */ + } + + return this.context.drizzle.transaction(async (tx) => { + let id: number | null; + if (supportsReturning(this.context.dialect)) { + // eslint-disable-next-line no-restricted-syntax -- guarded by the check above + const rows = await tx + .insert(termixIdentityCa) + .values(draft) + .returning(); + id = rows[0]?.id ?? null; + } else { + id = insertedId(await tx.insert(termixIdentityCa).values(draft)); + } + + if (id === null) { + throw new Error("Insert into termix_identity_ca returned no id."); + } + + await tx + .update(termixIdentityCa) + .set({ privateKey: seal(id) }) + .where(eq(termixIdentityCa.id, id)); + + const [row] = await tx + .select() + .from(termixIdentityCa) + .where(eq(termixIdentityCa.id, id)); + return row; + }); + } + async updateEncryptedForIdentity( userId: string, identityId: number, @@ -103,43 +164,42 @@ export class TermixIdentityCaRepository { ).privateKey : undefined; - const rows = await this.context.drizzle - .update(termixIdentityCa) - .set({ + const rows = await updateReturning( + this.context, + termixIdentityCa, + { ...update, ...(encryptedPrivateKey ? { privateKey: encryptedPrivateKey } : {}), - }) - .where(eq(termixIdentityCa.identityId, identityId)) - .returning(); + }, + eq(termixIdentityCa.identityId, identityId), + ); await this.afterWrite(); return this.decryptOne(rows[0] ?? null, userId); } async deleteByIdentityId(identityId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(termixIdentityCa) - .where(eq(termixIdentityCa.identityId, identityId)) - .returning({ id: termixIdentityCa.id }); + .where(eq(termixIdentityCa.identityId, identityId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(termixIdentityCa) - .where(eq(termixIdentityCa.userId, userId)) - .returning({ id: termixIdentityCa.id }); + .where(eq(termixIdentityCa.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private decryptOne>( diff --git a/src/backend/database/repositories/termix-identity-repository.ts b/src/backend/database/repositories/termix-identity-repository.ts index 9badeed..eae585f 100644 --- a/src/backend/database/repositories/termix-identity-repository.ts +++ b/src/backend/database/repositories/termix-identity-repository.ts @@ -1,6 +1,8 @@ import { and, asc, eq } from "drizzle-orm"; import { termixIdentities, termixIdentityKeys } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; export type TermixIdentityRecord = typeof termixIdentities.$inferSelect; export type NewTermixIdentityRecord = typeof termixIdentities.$inferInsert; @@ -57,10 +59,11 @@ export class TermixIdentityRepository { async createIdentity( identity: NewTermixIdentityRecord, ): Promise { - const rows = await this.context.drizzle - .insert(termixIdentities) - .values(identity) - .returning(); + const rows = await insertReturning( + this.context, + termixIdentities, + identity, + ); await this.afterWrite(); return rows[0]; @@ -70,11 +73,12 @@ export class TermixIdentityRepository { userId: string, update: TermixIdentityUpdate, ): Promise { - const rows = await this.context.drizzle - .update(termixIdentities) - .set(update) - .where(eq(termixIdentities.userId, userId)) - .returning(); + const rows = await updateReturning( + this.context, + termixIdentities, + update, + eq(termixIdentities.userId, userId), + ); if (rows.length > 0) { await this.afterWrite(); @@ -84,39 +88,36 @@ export class TermixIdentityRepository { } async deleteIdentityForUser(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(termixIdentities) - .where(eq(termixIdentities.userId, userId)) - .returning({ id: termixIdentities.id }); + .where(eq(termixIdentities.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserId(userId: string): Promise<{ identitiesDeleted: number; keysDeleted: number; }> { - const keyRows = await this.context.drizzle + const keyResult = await this.context.drizzle .delete(termixIdentityKeys) - .where(eq(termixIdentityKeys.userId, userId)) - .returning({ id: termixIdentityKeys.id }); + .where(eq(termixIdentityKeys.userId, userId)); - const identityRows = await this.context.drizzle + const result = await this.context.drizzle .delete(termixIdentities) - .where(eq(termixIdentities.userId, userId)) - .returning({ id: termixIdentities.id }); + .where(eq(termixIdentities.userId, userId)); - if (keyRows.length > 0 || identityRows.length > 0) { + if (rowsAffected(keyResult) > 0 || rowsAffected(result) > 0) { await this.afterWrite(); } return { - identitiesDeleted: identityRows.length, - keysDeleted: keyRows.length, + identitiesDeleted: rowsAffected(result), + keysDeleted: rowsAffected(keyResult), }; } @@ -170,10 +171,7 @@ export class TermixIdentityRepository { async createKey( key: NewTermixIdentityKeyRecord, ): Promise { - const rows = await this.context.drizzle - .insert(termixIdentityKeys) - .values(key) - .returning(); + const rows = await insertReturning(this.context, termixIdentityKeys, key); await this.afterWrite(); return rows[0]; @@ -184,16 +182,12 @@ export class TermixIdentityRepository { id: number, update: TermixIdentityKeyUpdate, ): Promise { - const rows = await this.context.drizzle - .update(termixIdentityKeys) - .set(update) - .where( - and( - eq(termixIdentityKeys.id, id), - eq(termixIdentityKeys.userId, userId), - ), - ) - .returning(); + const rows = await updateReturning( + this.context, + termixIdentityKeys, + update, + and(eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId)), + ); if (rows.length > 0) { await this.afterWrite(); @@ -203,21 +197,20 @@ export class TermixIdentityRepository { } async deleteKeyForUser(userId: string, id: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(termixIdentityKeys) .where( and( eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId), ), - ) - .returning({ id: termixIdentityKeys.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async findKeyForUser( diff --git a/src/backend/database/repositories/tmux-session-tag-repository.ts b/src/backend/database/repositories/tmux-session-tag-repository.ts index 3d89d85..60735f0 100644 --- a/src/backend/database/repositories/tmux-session-tag-repository.ts +++ b/src/backend/database/repositories/tmux-session-tag-repository.ts @@ -1,6 +1,7 @@ import { and, eq } from "drizzle-orm"; import { tmuxSessionTags } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; export type TmuxSessionTagRecord = typeof tmuxSessionTags.$inferSelect; @@ -45,7 +46,7 @@ export class TmuxSessionTagRepository { sessionName: string, newSessionName: string, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(tmuxSessionTags) .set({ sessionName: newSessionName }) .where( @@ -53,35 +54,33 @@ export class TmuxSessionTagRepository { eq(tmuxSessionTags.hostId, hostId), eq(tmuxSessionTags.sessionName, sessionName), ), - ) - .returning({ id: tmuxSessionTags.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteSessionForHost( hostId: number, sessionName: string, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(tmuxSessionTags) .where( and( eq(tmuxSessionTags.hostId, hostId), eq(tmuxSessionTags.sessionName, sessionName), ), - ) - .returning({ id: tmuxSessionTags.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async replaceForUserHostSession( @@ -90,7 +89,7 @@ export class TmuxSessionTagRepository { sessionName: string, tags: string[], ): Promise { - const deletedRows = await this.context.drizzle + const result = await this.context.drizzle .delete(tmuxSessionTags) .where( and( @@ -98,8 +97,7 @@ export class TmuxSessionTagRepository { eq(tmuxSessionTags.hostId, hostId), eq(tmuxSessionTags.sessionName, sessionName), ), - ) - .returning({ id: tmuxSessionTags.id }); + ); if (tags.length > 0) { await this.context.drizzle.insert(tmuxSessionTags).values( @@ -112,7 +110,7 @@ export class TmuxSessionTagRepository { ); } - const changedRows = deletedRows.length + tags.length; + const changedRows = rowsAffected(result) + tags.length; if (changedRows > 0) { await this.afterWrite(); } @@ -121,16 +119,15 @@ export class TmuxSessionTagRepository { } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(tmuxSessionTags) - .where(eq(tmuxSessionTags.userId, userId)) - .returning({ id: tmuxSessionTags.id }); + .where(eq(tmuxSessionTags.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/transfer-recent-repository.ts b/src/backend/database/repositories/transfer-recent-repository.ts index 309b270..de1266e 100644 --- a/src/backend/database/repositories/transfer-recent-repository.ts +++ b/src/backend/database/repositories/transfer-recent-repository.ts @@ -1,6 +1,7 @@ import { and, desc, eq, inArray, or } from "drizzle-orm"; import { transferRecent } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; export type TransferRecentRecord = typeof transferRecent.$inferSelect; @@ -100,47 +101,44 @@ export class TransferRecentRepository { return 0; } - const deleted = await this.context.drizzle + const result = await this.context.drizzle .delete(transferRecent) - .where(inArray(transferRecent.id, idsToDelete)) - .returning({ id: transferRecent.id }); + .where(inArray(transferRecent.id, idsToDelete)); - if (deleted.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return deleted.length; + return rowsAffected(result); } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(transferRecent) - .where(eq(transferRecent.userId, userId)) - .returning({ id: transferRecent.id }); + .where(eq(transferRecent.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostId(hostId: number): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(transferRecent) .where( or( eq(transferRecent.sourceHostId, hostId), eq(transferRecent.destHostId, hostId), ), - ) - .returning({ id: transferRecent.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } async deleteByHostIds(hostIds: number[]): Promise { @@ -148,21 +146,20 @@ export class TransferRecentRepository { return 0; } - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(transferRecent) .where( or( inArray(transferRecent.sourceHostId, hostIds), inArray(transferRecent.destHostId, hostIds), ), - ) - .returning({ id: transferRecent.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/ui-preference-repository.ts b/src/backend/database/repositories/ui-preference-repository.ts new file mode 100644 index 0000000..f4964e9 --- /dev/null +++ b/src/backend/database/repositories/ui-preference-repository.ts @@ -0,0 +1,68 @@ +import { eq } from "drizzle-orm"; +import { uiPreferences } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturningWhere, updateReturning } from "./returning.js"; + +export type UiPreferenceRecord = typeof uiPreferences.$inferSelect; + +export class UiPreferenceRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async findByUserId(userId: string): Promise { + const rows = await this.context.drizzle + .select() + .from(uiPreferences) + .where(eq(uiPreferences.userId, userId)) + .limit(1); + + return rows[0] ?? null; + } + + async upsert( + userId: string, + data: string, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findByUserId(userId); + + if (!existing) { + const rows = await insertReturningWhere( + this.context, + uiPreferences, + { userId, data, updatedAt: now }, + eq(uiPreferences.userId, userId), + ); + await this.afterWrite(); + return rows[0]; + } + + const rows = await updateReturning( + this.context, + uiPreferences, + { data, updatedAt: now }, + eq(uiPreferences.userId, userId), + ); + await this.afterWrite(); + return rows[0]; + } + + async deleteByUserId(userId: string): Promise { + const result = await this.context.drizzle + .delete(uiPreferences) + .where(eq(uiPreferences.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + + return rowsAffected(result); + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/repositories/user-preference-repository.ts b/src/backend/database/repositories/user-preference-repository.ts index 7163c28..6a37a03 100644 --- a/src/backend/database/repositories/user-preference-repository.ts +++ b/src/backend/database/repositories/user-preference-repository.ts @@ -1,6 +1,8 @@ import { eq } from "drizzle-orm"; import { userPreferences } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturningWhere, updateReturning } from "./returning.js"; export type UserPreferenceRecord = typeof userPreferences.$inferSelect; export type NewUserPreferenceRecord = typeof userPreferences.$inferInsert; @@ -31,34 +33,36 @@ export class UserPreferenceRepository { const existing = await this.findByUserId(userId); if (!existing) { - const rows = await this.context.drizzle - .insert(userPreferences) - .values({ userId, ...update }) - .returning(); + const rows = await insertReturningWhere( + this.context, + userPreferences, + { userId, ...update }, + eq(userPreferences.userId, userId), + ); await this.afterWrite(); return rows[0]; } - const rows = await this.context.drizzle - .update(userPreferences) - .set(update) - .where(eq(userPreferences.userId, userId)) - .returning(); + const rows = await updateReturning( + this.context, + userPreferences, + update, + eq(userPreferences.userId, userId), + ); await this.afterWrite(); return rows[0]; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(userPreferences) - .where(eq(userPreferences.userId, userId)) - .returning({ userId: userPreferences.userId }); + .where(eq(userPreferences.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/user-repository.ts b/src/backend/database/repositories/user-repository.ts index 68442f4..2cc71fd 100644 --- a/src/backend/database/repositories/user-repository.ts +++ b/src/backend/database/repositories/user-repository.ts @@ -1,6 +1,12 @@ -import { eq, inArray } from "drizzle-orm"; +import { asc, eq, inArray, like, sql } from "drizzle-orm"; import { users } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { + countValue, + rowsAffected, + supportsReturning, +} from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; export type UserRecord = typeof users.$inferSelect; export type NewUserRecord = typeof users.$inferInsert; @@ -17,6 +23,42 @@ export class UserRepository { return this.context.drizzle.select().from(users); } + /** + * One page of users, optionally filtered by username. + * + * The admin panel used to render every account at once, which is fine at + * home and not fine on a directory-backed install with thousands of them. + * Sorted by username so paging is stable between requests. + */ + async listPage(input: { + search?: string; + limit: number; + offset: number; + }): Promise<{ users: UserRecord[]; total: number }> { + const term = input.search?.trim(); + const where = term + ? like(sql`lower(${users.username})`, `%${term.toLowerCase()}%`) + : undefined; + + const [rows, totalResult] = await Promise.all([ + this.context.drizzle + .select() + .from(users) + .where(where) + // Case-insensitive: a plain sort puts every capitalised name ahead of + // every lowercase one, which reads as unordered in the admin list. + .orderBy(asc(sql`lower(${users.username})`)) + .limit(input.limit) + .offset(input.offset), + this.context.drizzle + .select({ count: sql`COUNT(*)` }) + .from(users) + .where(where), + ]); + + return { users: rows, total: countValue(totalResult[0]?.count) }; + } + async findById(id: string): Promise { const rows = await this.context.drizzle .select() @@ -62,10 +104,7 @@ export class UserRepository { } async create(user: NewUserRecord): Promise { - const rows = await this.context.drizzle - .insert(users) - .values(user) - .returning(); + const rows = await insertReturning(this.context, users, user); await this.afterWrite(); return rows[0]; } @@ -73,17 +112,10 @@ export class UserRepository { async createFirstLocalUser( user: NewFirstLocalUserRecord, ): Promise<{ user: UserRecord; isFirstUser: boolean }> { - const result = this.context.drizzle.transaction((tx) => { - const existingUsers = tx.select({ id: users.id }).from(users).all(); - const isFirstUser = existingUsers.length === 0; - const rows = tx - .insert(users) - .values({ ...user, isAdmin: isFirstUser }) - .returning() - .all(); - - return { user: rows[0], isFirstUser }; - }); + const result = await this.createCheckingIfFirst((isFirstUser) => ({ + ...user, + isAdmin: isFirstUser, + })); await this.afterWrite(); return result; @@ -92,41 +124,87 @@ export class UserRepository { async createFirstSsoUser( user: NewUserRecord, ): Promise<{ user: UserRecord; isFirstUser: boolean }> { - const result = this.context.drizzle.transaction((tx) => { - const existingUsers = tx.select({ id: users.id }).from(users).all(); - const isFirstUser = existingUsers.length === 0; - const rows = tx - .insert(users) - .values({ ...user, isAdmin: isFirstUser || Boolean(user.isAdmin) }) - .returning() - .all(); - - return { user: rows[0], isFirstUser }; - }); + const result = await this.createCheckingIfFirst((isFirstUser) => ({ + ...user, + isAdmin: isFirstUser || Boolean(user.isAdmin), + })); await this.afterWrite(); return result; } + /** + * Creates a user, making them an admin if the table was empty. + * + * The check and the insert have to be one transaction: two people signing up + * at once would otherwise both see an empty table and both become admin. + * + * The two branches are not a style choice. better-sqlite3 is synchronous and + * rejects an async transaction callback outright โ€” "Transaction function + * cannot return a promise" โ€” so a single body cannot serve both. It fails + * loudly rather than silently skipping the write, which is the one mercy here. + */ + private async createCheckingIfFirst( + build: (isFirstUser: boolean) => NewUserRecord, + ): Promise<{ user: UserRecord; isFirstUser: boolean }> { + if (this.context.dialect === "sqlite") { + /* eslint-disable no-restricted-syntax -- sqlite-only branch: the dialect + is checked directly above, and better-sqlite3 rejects an async + transaction callback, so this cannot use the shared helpers. */ + return this.context.drizzle.transaction((tx) => { + const isFirstUser = + tx.select({ id: users.id }).from(users).all().length === 0; + const rows = tx + .insert(users) + .values(build(isFirstUser)) + .returning() + .all(); + return { user: rows[0], isFirstUser }; + }); + /* eslint-enable no-restricted-syntax */ + } + + return this.context.drizzle.transaction(async (tx) => { + const existing = await tx.select({ id: users.id }).from(users); + const isFirstUser = existing.length === 0; + const values = build(isFirstUser); + + if (supportsReturning(this.context.dialect)) { + // eslint-disable-next-line no-restricted-syntax -- guarded by the check on this line + const rows = await tx.insert(users).values(values).returning(); + return { user: rows[0], isFirstUser }; + } + + // users is keyed by a text id the caller supplies, so there is something + // to read back by even without RETURNING. + await tx.insert(users).values(values); + const [user] = await tx + .select() + .from(users) + .where(eq(users.id, values.id)); + return { user, isFirstUser }; + }); + } + async update(id: string, update: UserUpdate): Promise { - const rows = await this.context.drizzle - .update(users) - .set(update) - .where(eq(users.id, id)) - .returning(); + const rows = await updateReturning( + this.context, + users, + update, + eq(users.id, id), + ); await this.afterWrite(); return rows[0] ?? null; } async delete(id: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(users) - .where(eq(users.id, id)) - .returning({ id: users.id }); + .where(eq(users.id, id)); await this.afterWrite(); - return rows.length > 0; + return rowsAffected(result) > 0; } async countAdmins(): Promise { diff --git a/src/backend/database/repositories/vault-profile-repository.ts b/src/backend/database/repositories/vault-profile-repository.ts index 319a7b4..bcf8cec 100644 --- a/src/backend/database/repositories/vault-profile-repository.ts +++ b/src/backend/database/repositories/vault-profile-repository.ts @@ -1,6 +1,13 @@ import { desc, eq, or } from "drizzle-orm"; +import { randomUUID } from "crypto"; import { vaultProfiles } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { + deleteReturning, + insertReturning, + updateReturning, +} from "./returning.js"; export type VaultProfileRecord = typeof vaultProfiles.$inferSelect; @@ -44,25 +51,23 @@ export class VaultProfileRepository { } async create(input: VaultProfileCreateInput): Promise { - const [created] = await this.context.drizzle - .insert(vaultProfiles) - .values({ - userId: input.userId, - name: input.name, - description: input.description, - folder: input.folder, - tags: input.tags, - vaultAddr: input.vaultAddr, - vaultNamespace: input.vaultNamespace, - oidcMount: input.oidcMount, - oidcRole: input.oidcRole, - sshMount: input.sshMount, - sshRole: input.sshRole, - validPrincipals: input.validPrincipals, - keyType: input.keyType, - shared: input.shared ?? false, - }) - .returning(); + const [created] = await insertReturning(this.context, vaultProfiles, { + syncId: randomUUID(), + userId: input.userId, + name: input.name, + description: input.description, + folder: input.folder, + tags: input.tags, + vaultAddr: input.vaultAddr, + vaultNamespace: input.vaultNamespace, + oidcMount: input.oidcMount, + oidcRole: input.oidcRole, + sshMount: input.sshMount, + sshRole: input.sshRole, + validPrincipals: input.validPrincipals, + keyType: input.keyType, + shared: input.shared ?? false, + }); await this.afterWrite(); return created; @@ -82,14 +87,15 @@ export class VaultProfileRepository { id: number, input: VaultProfileUpdateInput, ): Promise { - const [updated] = await this.context.drizzle - .update(vaultProfiles) - .set({ + const [updated] = await updateReturning( + this.context, + vaultProfiles, + { ...input, updatedAt: input.updatedAt ?? new Date().toISOString(), - }) - .where(eq(vaultProfiles.id, id)) - .returning(); + }, + eq(vaultProfiles.id, id), + ); if (updated) { await this.afterWrite(); @@ -98,30 +104,28 @@ export class VaultProfileRepository { return updated ?? null; } - async deleteById(id: number): Promise { - const rows = await this.context.drizzle - .delete(vaultProfiles) - .where(eq(vaultProfiles.id, id)) - .returning({ id: vaultProfiles.id }); + async deleteById(id: number): Promise<{ syncId: string | null } | null> { + const rows = await deleteReturning( + this.context, + vaultProfiles, + eq(vaultProfiles.id, id), + ); - if (rows.length > 0) { - await this.afterWrite(); - } - - return rows.length > 0; + if (rows.length === 0) return null; + await this.afterWrite(); + return { syncId: rows[0].syncId }; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(vaultProfiles) - .where(eq(vaultProfiles.userId, userId)) - .returning({ id: vaultProfiles.id }); + .where(eq(vaultProfiles.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/vault-token-repository.ts b/src/backend/database/repositories/vault-token-repository.ts index 1db262f..a14ea70 100644 --- a/src/backend/database/repositories/vault-token-repository.ts +++ b/src/backend/database/repositories/vault-token-repository.ts @@ -1,6 +1,8 @@ import { and, eq } from "drizzle-orm"; import { vaultTokens } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { upsert } from "./returning.js"; export type VaultTokenRecord = typeof vaultTokens.$inferSelect; @@ -22,16 +24,17 @@ export class VaultTokenRepository { async upsert(input: VaultTokenUpsertInput): Promise { const createdAt = input.createdAt ?? new Date().toISOString(); - await this.context.drizzle - .insert(vaultTokens) - .values({ + await upsert( + this.context, + vaultTokens, + { userId: input.userId, profileId: input.profileId, sshCert: input.sshCert, privateKey: input.privateKey, expiresAt: input.expiresAt, - }) - .onConflictDoUpdate({ + }, + { target: [vaultTokens.userId, vaultTokens.profileId], set: { sshCert: input.sshCert, @@ -39,7 +42,8 @@ export class VaultTokenRepository { expiresAt: input.expiresAt, createdAt, }, - }); + }, + ); await this.afterWrite(); } @@ -67,7 +71,7 @@ export class VaultTokenRepository { profileId: number, lastUsed = new Date().toISOString(), ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .update(vaultTokens) .set({ lastUsed }) .where( @@ -75,48 +79,45 @@ export class VaultTokenRepository { eq(vaultTokens.userId, userId), eq(vaultTokens.profileId, profileId), ), - ) - .returning({ id: vaultTokens.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserAndProfile( userId: string, profileId: number, ): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(vaultTokens) .where( and( eq(vaultTokens.userId, userId), eq(vaultTokens.profileId, profileId), ), - ) - .returning({ id: vaultTokens.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } async deleteByUserId(userId: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(vaultTokens) - .where(eq(vaultTokens.userId, userId)) - .returning({ id: vaultTokens.id }); + .where(eq(vaultTokens.userId, userId)); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length; + return rowsAffected(result); } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/webauthn-credential-repository.ts b/src/backend/database/repositories/webauthn-credential-repository.ts index f5dac86..5810cad 100644 --- a/src/backend/database/repositories/webauthn-credential-repository.ts +++ b/src/backend/database/repositories/webauthn-credential-repository.ts @@ -1,6 +1,8 @@ import { and, eq } from "drizzle-orm"; import { webauthnCredentials } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning } from "./returning.js"; export type WebauthnCredentialRecord = typeof webauthnCredentials.$inferSelect; export type NewWebauthnCredentialRecord = @@ -41,10 +43,11 @@ export class WebauthnCredentialRepository { async create( record: NewWebauthnCredentialRecord, ): Promise { - const rows = await this.context.drizzle - .insert(webauthnCredentials) - .values(record) - .returning(); + const rows = await insertReturning( + this.context, + webauthnCredentials, + record, + ); await this.afterWrite(); return rows[0]; @@ -63,21 +66,20 @@ export class WebauthnCredentialRepository { } async deleteForUser(userId: string, id: string): Promise { - const rows = await this.context.drizzle + const result = await this.context.drizzle .delete(webauthnCredentials) .where( and( eq(webauthnCredentials.id, id), eq(webauthnCredentials.userId, userId), ), - ) - .returning({ id: webauthnCredentials.id }); + ); - if (rows.length > 0) { + if (rowsAffected(result) > 0) { await this.afterWrite(); } - return rows.length > 0; + return rowsAffected(result) > 0; } private async afterWrite(): Promise { diff --git a/src/backend/database/repositories/workspace-repository.ts b/src/backend/database/repositories/workspace-repository.ts new file mode 100644 index 0000000..25c88b9 --- /dev/null +++ b/src/backend/database/repositories/workspace-repository.ts @@ -0,0 +1,266 @@ +import { and, eq, ne } from "drizzle-orm"; +import { randomUUID } from "crypto"; +import { userWorkspaces } from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; +import { rowsAffected } from "./mutation-result.js"; +import { insertReturning, updateReturning } from "./returning.js"; + +export type WorkspaceRecord = typeof userWorkspaces.$inferSelect; + +export interface WorkspaceCreateInput { + name: string; + color?: string | null; + icon?: string | null; + payload: string; +} + +export interface WorkspaceUpdateInput { + name?: string; + color?: string | null; + icon?: string | null; +} + +export class WorkspaceRepository { + constructor( + private readonly context: DatabaseContext, + private readonly onWrite?: () => void | Promise, + ) {} + + async listByUser(userId: string): Promise { + return this.context.drizzle + .select() + .from(userWorkspaces) + .where(eq(userWorkspaces.userId, userId)); + } + + async findById(userId: string, id: number): Promise { + const rows = await this.context.drizzle + .select() + .from(userWorkspaces) + .where(and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId))) + .limit(1); + return rows[0] ?? null; + } + + async findLastSession(userId: string): Promise { + const rows = await this.context.drizzle + .select() + .from(userWorkspaces) + .where( + and( + eq(userWorkspaces.userId, userId), + eq(userWorkspaces.kind, "last_session"), + ), + ) + .limit(1); + return rows[0] ?? null; + } + + async findDefault(userId: string): Promise { + const rows = await this.context.drizzle + .select() + .from(userWorkspaces) + .where( + and( + eq(userWorkspaces.userId, userId), + eq(userWorkspaces.isDefault, true), + ), + ) + .limit(1); + return rows[0] ?? null; + } + + async upsertLastSession( + userId: string, + payload: string, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findLastSession(userId); + + if (!existing) { + const [created] = await insertReturning(this.context, userWorkspaces, { + userId, + name: "Last Session", + color: null, + icon: null, + kind: "last_session", + isDefault: false, + payload, + syncId: randomUUID(), + createdAt: now, + updatedAt: now, + }); + await this.afterWrite(); + return created; + } + + const [updated] = await updateReturning( + this.context, + userWorkspaces, + { payload, updatedAt: now }, + and( + eq(userWorkspaces.id, existing.id), + eq(userWorkspaces.userId, userId), + ), + ); + await this.afterWrite(); + return updated; + } + + async create( + userId: string, + input: WorkspaceCreateInput, + now = new Date().toISOString(), + ): Promise { + const [created] = await insertReturning(this.context, userWorkspaces, { + userId, + name: input.name, + color: input.color ?? null, + icon: input.icon ?? null, + kind: "manual", + isDefault: false, + payload: input.payload, + syncId: randomUUID(), + createdAt: now, + updatedAt: now, + }); + await this.afterWrite(); + return created; + } + + async update( + userId: string, + id: number, + input: WorkspaceUpdateInput, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findById(userId, id); + if (!existing || existing.kind !== "manual") return null; + + const [updated] = await updateReturning( + this.context, + userWorkspaces, + { + name: input.name ?? existing.name, + color: input.color === undefined ? existing.color : input.color, + icon: input.icon === undefined ? existing.icon : input.icon, + updatedAt: now, + }, + and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)), + ); + await this.afterWrite(); + return updated ?? null; + } + + async updateContent( + userId: string, + id: number, + payload: string, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findById(userId, id); + if (!existing || existing.kind !== "manual") return null; + + const [updated] = await updateReturning( + this.context, + userWorkspaces, + { payload, updatedAt: now }, + and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)), + ); + await this.afterWrite(); + return updated ?? null; + } + + async setDefault( + userId: string, + id: number, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findById(userId, id); + if (!existing || existing.kind !== "manual") return null; + + await this.context.drizzle + .update(userWorkspaces) + .set({ isDefault: false, updatedAt: now }) + .where( + and( + eq(userWorkspaces.userId, userId), + eq(userWorkspaces.isDefault, true), + ne(userWorkspaces.id, id), + ), + ); + + const [updated] = await updateReturning( + this.context, + userWorkspaces, + { isDefault: true, updatedAt: now }, + and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)), + ); + await this.afterWrite(); + return updated ?? null; + } + + async unsetDefault( + userId: string, + id: number, + now = new Date().toISOString(), + ): Promise { + const existing = await this.findById(userId, id); + if (!existing || existing.kind !== "manual") return null; + + const [updated] = await updateReturning( + this.context, + userWorkspaces, + { isDefault: false, updatedAt: now }, + and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)), + ); + await this.afterWrite(); + return updated ?? null; + } + + async touchLastUsed( + userId: string, + id: number, + now = new Date().toISOString(), + ): Promise { + const result = await this.context.drizzle + .update(userWorkspaces) + .set({ lastUsedAt: now }) + .where(and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId))); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + } + + async delete(userId: string, id: number): Promise { + const existing = await this.findById(userId, id); + if (!existing || existing.kind !== "manual") return false; + + const result = await this.context.drizzle + .delete(userWorkspaces) + .where(and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId))); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + return true; + } + return false; + } + + async deleteByUserId(userId: string): Promise { + const owned = await this.listByUser(userId); + const result = await this.context.drizzle + .delete(userWorkspaces) + .where(eq(userWorkspaces.userId, userId)); + + if (rowsAffected(result) > 0) { + await this.afterWrite(); + } + return owned.length; + } + + private async afterWrite(): Promise { + await this.onWrite?.(); + } +} diff --git a/src/backend/database/routes/acme-ssl-routes.ts b/src/backend/database/routes/acme-ssl-routes.ts index 317a08a..431af8c 100644 --- a/src/backend/database/routes/acme-ssl-routes.ts +++ b/src/backend/database/routes/acme-ssl-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import { execFileSync } from "child_process"; import { promises as fs } from "fs"; import path from "path"; @@ -5,6 +6,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { RequestHandler, Router } from "express"; import { authLogger } from "../../utils/logger.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; +import { reloadNginxWithSSL } from "../../utils/nginx-ssl-reload.js"; import { createCurrentSettingsRepository, createCurrentUserRepository, @@ -28,7 +30,7 @@ export type AcmeSettings = { enabled: boolean; domain: string; email: string; - challengeType: "http-webroot" | "dns-cloudflare"; + challengeType: "http-webroot" | "dns-cloudflare" | "manual"; cloudflareToken: string; lastIssuedAt: string | null; certStatus: "none" | "valid" | "expiring" | "expired"; @@ -166,7 +168,7 @@ export function registerAcmeSSLRoutes( * type: string * challengeType: * type: string - * enum: [http-webroot, dns-cloudflare] + * enum: [http-webroot, dns-cloudflare, manual] * cloudflareToken: * type: string * responses: @@ -382,6 +384,8 @@ export function registerAcmeSSLRoutes( operation: "acme_cert_installed", }); + const reload = reloadNginxWithSSL(); + const { ipAddress, userAgent } = getRequestMeta(req); await logAudit({ userId, @@ -394,9 +398,13 @@ export function registerAcmeSSLRoutes( success: true, }); - res.json({ success: true, ...(await getAcmeSettings()) }); + res.json({ + success: true, + reloadMessage: reload.message, + ...(await getAcmeSettings()), + }); } catch (err) { - const message = err instanceof Error ? err.message : "Unknown error"; + const message = getErrorMessage(err); authLogger.error("ACME certificate request failed", err); const { ipAddress, userAgent } = getRequestMeta(req); @@ -414,4 +422,165 @@ export function registerAcmeSSLRoutes( res.status(500).json({ error: `Certificate request failed: ${message}` }); } }); + + /** + * @openapi + * /users/manual-ssl-upload: + * post: + * summary: Upload a manual/custom SSL certificate and key (admin only) + * description: Validates and installs a user-supplied PEM certificate and private key as the active Termix SSL certificate. + * tags: + * - Users + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * certificate: + * type: string + * privateKey: + * type: string + * responses: + * 200: + * description: Certificate uploaded and installed successfully. + * 400: + * description: Invalid or missing certificate/key. + * 403: + * description: Not authorized. + * 500: + * description: Certificate installation failed. + */ + router.post("/manual-ssl-upload", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + const actor = await getAdminActor(userId); + try { + if (!actor) { + return res.status(403).json({ error: "Not authorized" }); + } + + const { certificate, privateKey } = req.body; + + if ( + typeof certificate !== "string" || + typeof privateKey !== "string" || + !certificate.includes("BEGIN CERTIFICATE") || + !privateKey.includes("PRIVATE KEY") + ) { + return res.status(400).json({ + error: "A valid PEM certificate and private key are required", + }); + } + + await fs.mkdir(SSL_DIR, { recursive: true }); + + const tmpCertFile = path.join(SSL_DIR, ".manual-upload.crt.tmp"); + const tmpKeyFile = path.join(SSL_DIR, ".manual-upload.key.tmp"); + + try { + await fs.writeFile(tmpCertFile, certificate, { mode: 0o644 }); + await fs.writeFile(tmpKeyFile, privateKey, { mode: 0o600 }); + + try { + execFileSync("openssl", ["x509", "-in", tmpCertFile, "-noout"], { + stdio: "pipe", + }); + execFileSync( + "openssl", + ["pkey", "-in", tmpKeyFile, "-noout", "-check"], + { stdio: "pipe" }, + ); + } catch { + return res.status(400).json({ + error: + "The provided certificate or private key is not valid PEM data", + }); + } + + const certPubkey = execFileSync( + "openssl", + ["x509", "-in", tmpCertFile, "-noout", "-pubkey"], + { stdio: "pipe" }, + ); + const keyPubkey = execFileSync( + "openssl", + ["pkey", "-in", tmpKeyFile, "-pubout"], + { stdio: "pipe" }, + ); + + if (!certPubkey.equals(keyPubkey)) { + return res + .status(400) + .json({ error: "The certificate and private key do not match" }); + } + + const certDest = path.join(SSL_DIR, "termix.crt"); + const keyDest = path.join(SSL_DIR, "termix.key"); + await fs.rename(tmpCertFile, certDest); + await fs.rename(tmpKeyFile, keyDest); + await fs.chmod(keyDest, 0o600); + await fs.chmod(certDest, 0o644); + } finally { + await fs.rm(tmpCertFile, { force: true }); + await fs.rm(tmpKeyFile, { force: true }); + } + + const settingsRepository = createCurrentSettingsRepository(); + const existing = await settingsRepository.get("acme_ssl_settings"); + const current = existing ? JSON.parse(existing) : {}; + const updated = { + ...current, + challengeType: "manual", + lastIssuedAt: new Date().toISOString(), + }; + await settingsRepository.set( + "acme_ssl_settings", + JSON.stringify(updated), + ); + + authLogger.info("Manual SSL certificate installed", { + operation: "manual_ssl_installed", + }); + + const reload = reloadNginxWithSSL(); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: actor.username ?? userId, + action: "manual_ssl_upload", + resourceType: "setting", + details: JSON.stringify({ success: true }), + ipAddress, + userAgent, + success: true, + }); + + res.json({ + success: true, + reloadMessage: reload.message, + ...(await getAcmeSettings()), + }); + } catch (err) { + const message = getErrorMessage(err); + authLogger.error("Manual SSL certificate upload failed", err); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: actor?.username ?? userId, + action: "manual_ssl_upload", + resourceType: "setting", + details: JSON.stringify({ error: message }), + ipAddress, + userAgent, + success: false, + }); + + res + .status(500) + .json({ error: `Certificate installation failed: ${message}` }); + } + }); } diff --git a/src/backend/database/routes/alert-rules-routes.ts b/src/backend/database/routes/alert-rules-routes.ts index 5ce379a..6bfdbba 100644 --- a/src/backend/database/routes/alert-rules-routes.ts +++ b/src/backend/database/routes/alert-rules-routes.ts @@ -4,6 +4,7 @@ import { createCurrentAlertRepository } from "../repositories/factory.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { databaseLogger } from "../../utils/logger.js"; import { sendWebhook, sendNtfy } from "../../utils/notification-sender.js"; +import { sendDiscord } from "../../utils/discord-sender.js"; const router = express.Router(); const authManager = AuthManager.getInstance(); @@ -70,8 +71,10 @@ router.post("/notification-channels", async (req, res) => { if (!name || typeof name !== "string" || !name.trim()) { return res.status(400).json({ error: "name is required" }); } - if (type !== "webhook" && type !== "ntfy") { - return res.status(400).json({ error: "type must be 'webhook' or 'ntfy'" }); + if (type !== "webhook" && type !== "ntfy" && type !== "discord") { + return res + .status(400) + .json({ error: "type must be 'webhook', 'ntfy' or 'discord'" }); } if (!config || typeof config !== "object") { return res.status(400).json({ error: "config is required" }); @@ -88,6 +91,21 @@ router.post("/notification-channels", async (req, res) => { if (!c.url || typeof c.url !== "string") return res.status(400).json({ error: "webhook config requires url" }); } + if (type === "discord") { + const c = config as Record; + if (!c.url || typeof c.url !== "string") + return res.status(400).json({ error: "discord config requires url" }); + if ( + !/^https:\/\/(?:canary\.|ptb\.)?(?:discord\.com|discordapp\.com)\/api\/webhooks\/.+/i.test( + c.url, + ) + ) { + return res.status(400).json({ + error: + "discord config requires a valid Discord webhook URL (https://discord.com/api/webhooks/...)", + }); + } + } try { const row = await createCurrentAlertRepository().createNotificationChannel({ @@ -134,10 +152,10 @@ router.put( ); if (!existing) return res.status(404).json({ error: "Channel not found" }); - if (type && type !== "webhook" && type !== "ntfy") { + if (type && type !== "webhook" && type !== "ntfy" && type !== "discord") { return res .status(400) - .json({ error: "type must be 'webhook' or 'ntfy'" }); + .json({ error: "type must be 'webhook', 'ntfy' or 'discord'" }); } if ( name === undefined && @@ -245,6 +263,11 @@ router.post( config as unknown as Parameters[0], testPayload, ); + } else if (row.type === "discord") { + await sendDiscord( + config as unknown as Parameters[0], + testPayload, + ); } res.json({ success: true }); } catch (err) { diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index 5cbff23..df196c8 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest, CacheEntry, @@ -87,7 +88,7 @@ async function fetchAlertsFromGitHub(): Promise { } catch (error) { authLogger.error("Failed to fetch alerts from GitHub", { operation: "alerts_fetch", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); return []; } diff --git a/src/backend/database/routes/audit-log-routes.ts b/src/backend/database/routes/audit-log-routes.ts index 91273b9..60dea30 100644 --- a/src/backend/database/routes/audit-log-routes.ts +++ b/src/backend/database/routes/audit-log-routes.ts @@ -5,6 +5,12 @@ import { createCurrentUserRepository, } from "../repositories/factory.js"; import { apiLogger } from "../../utils/logger.js"; +import { exportFilename, toCsv, toNdjson } from "../../utils/audit-export.js"; +import { + logAudit, + getAuditUsername, + getRequestMeta, +} from "../../utils/audit-logger.js"; async function isAdminUser(userId: string | undefined): Promise { if (!userId) return false; @@ -132,4 +138,125 @@ export function registerAuditLogRoutes( .json({ error: "Failed to fetch audit log actions" }); } }); + + /** + * @openapi + * /audit-logs/export: + * get: + * summary: Export audit logs + * description: Streams the full filtered result set as CSV or NDJSON. Accepts the same filters as GET /audit-logs. Admin only. The export is itself audited. + * tags: + * - Audit + * parameters: + * - in: query + * name: format + * schema: { type: string, enum: [csv, ndjson], default: csv } + * - in: query + * name: userId + * schema: { type: string } + * - in: query + * name: action + * schema: { type: string } + * - in: query + * name: resourceType + * schema: { type: string } + * - in: query + * name: success + * schema: { type: string, enum: [true, false] } + * - in: query + * name: startDate + * schema: { type: string, format: date-time } + * - in: query + * name: endDate + * schema: { type: string, format: date-time } + * responses: + * 200: + * description: Audit log file. + * 403: + * description: Not authorized. + * 500: + * description: Failed to export audit logs. + */ + router.get("/audit-logs/export", authenticateJWT, async (req, res) => { + const authReq = req as AuthenticatedRequest; + try { + if (!(await isAdminUser(authReq.userId))) { + return res.status(403).json({ error: "Not authorized" }); + } + + const format = req.query.format === "ndjson" ? "ndjson" : "csv"; + const { userId, action, resourceType, success, startDate, endDate } = + req.query as Record; + const filters = { + userId, + action, + resourceType, + success: + success !== undefined && success !== "" + ? success === "true" + : undefined, + startDate, + endDate, + }; + + res.setHeader( + "Content-Type", + format === "csv" ? "text/csv; charset=utf-8" : "application/x-ndjson", + ); + res.setHeader( + "Content-Disposition", + `attachment; filename="${exportFilename(format, new Date())}"`, + ); + + // Streamed in batches: an export is unbounded by definition, and the + // whole point is to move data out before retention drops it. + const BATCH = 500; + let offset = 0; + let exported = 0; + + for (;;) { + const rows = await createCurrentAuditLogRepository().listForExport({ + filters, + limit: BATCH, + offset, + }); + if (rows.length === 0) break; + + if (format === "csv") { + // Header only on the first batch. + const chunk = toCsv(rows); + res.write( + offset === 0 ? chunk : chunk.slice(chunk.indexOf("\n") + 1), + ); + } else { + res.write(toNdjson(rows)); + } + + exported += rows.length; + offset += rows.length; + if (rows.length < BATCH) break; + } + + res.end(); + + // Reading the whole trail is itself worth recording. + const { ipAddress, userAgent } = getRequestMeta(req); + void logAudit({ + userId: authReq.userId!, + username: await getAuditUsername(authReq.userId!), + action: "export_audit_logs", + resourceType: "audit_log", + details: JSON.stringify({ format, exported, filters }), + ipAddress, + userAgent, + success: true, + }); + } catch (err) { + apiLogger.error("Failed to export audit logs", err); + if (!res.headersSent) { + return res.status(500).json({ error: "Failed to export audit logs" }); + } + res.end(); + } + }); } diff --git a/src/backend/database/routes/automations.ts b/src/backend/database/routes/automations.ts new file mode 100644 index 0000000..a2a81c6 --- /dev/null +++ b/src/backend/database/routes/automations.ts @@ -0,0 +1,812 @@ +import crypto from "node:crypto"; +import express, { type Request, type Response } from "express"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import type { + AutomationDefinition, + Step, + Trigger, +} from "../../../types/automations.js"; +import { AUTOMATION_DEFINITION_VERSION } from "../../../types/automations.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { databaseLogger } from "../../utils/logger.js"; +import { + getAuditUsername, + getRequestMeta, + logAudit, +} from "../../utils/audit-logger.js"; +import { createCurrentAutomationRepository } from "../repositories/factory.js"; +import type { AutomationRow } from "../repositories/automation-repository.js"; +import { AutomationEngine } from "../../automations/engine.js"; +import { + computeNextDueAt, + isValidCron, + isValidTimezone, +} from "../../automations/cron.js"; + +const router = express.Router(); + +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); +const requireDataAccess = authManager.createDataAccessMiddleware(); + +const TRIGGER_KINDS = new Set([ + "metric_threshold", + "host_status", + "health_check", + "schedule", + "docker_event", + "internal_event", + "webhook", +]); + +const STEP_TYPES = new Set([ + "notify", + "http", + "run_snippet", + "run_command", + "docker", + "tunnel", + "wol", + "wait", + "set_var", + "if", + "run_automation", + "stop", +]); + +const OPERATORS = new Set([ + ">", + "<", + ">=", + "<=", + "==", + "!=", + "contains", + "not_contains", + "changed", +]); + +function parseId(raw: unknown): number | null { + const id = typeof raw === "string" ? parseInt(raw, 10) : NaN; + return Number.isInteger(id) && id > 0 ? id : null; +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +/** + * Validates a definition before it is stored. The engine treats the stored + * blob as trusted, so everything it relies on is checked once here. + */ +export function validateDefinition(value: unknown): { + ok: boolean; + error?: string; + definition?: AutomationDefinition; +} { + if (typeof value !== "object" || value === null) { + return { ok: false, error: "Definition must be an object" }; + } + + const candidate = value as Partial; + const trigger = candidate.trigger as Trigger | undefined; + + if (!trigger || !TRIGGER_KINDS.has(trigger.kind)) { + return { ok: false, error: "Unknown or missing trigger kind" }; + } + + if (trigger.kind === "metric_threshold") { + if (!OPERATORS.has(trigger.operator)) { + return { ok: false, error: "Unknown comparison operator" }; + } + if (typeof trigger.value !== "number" || !Number.isFinite(trigger.value)) { + return { ok: false, error: "Threshold value must be a number" }; + } + if (!trigger.metric?.path) { + return { ok: false, error: "Trigger is missing a metric" }; + } + } + + if (trigger.kind === "schedule") { + const hasInterval = + typeof trigger.intervalSeconds === "number" && + trigger.intervalSeconds > 0; + const hasCron = isNonEmptyString(trigger.cron); + if (!hasInterval && !hasCron) { + return { ok: false, error: "Schedule needs an interval or a cron" }; + } + if (hasCron && !isValidCron(trigger.cron as string)) { + return { ok: false, error: "Cron expression is not valid" }; + } + if (hasInterval && (trigger.intervalSeconds as number) < 60) { + return { ok: false, error: "Interval must be at least 60 seconds" }; + } + if ( + isNonEmptyString(trigger.timezone) && + !isValidTimezone(trigger.timezone) + ) { + return { ok: false, error: "Time zone is not valid" }; + } + } + + const steps = candidate.steps; + if (!Array.isArray(steps)) { + return { ok: false, error: "Definition must include a steps array" }; + } + + const seen = new Set(); + const stepError = validateSteps(steps as Step[], seen); + if (stepError) return { ok: false, error: stepError }; + + return { + ok: true, + definition: { + version: candidate.version ?? AUTOMATION_DEFINITION_VERSION, + trigger, + steps: steps as Step[], + }, + }; +} + +function validateSteps(steps: Step[], seen: Set): string | null { + for (const step of steps) { + if (!step || typeof step !== "object") return "Step must be an object"; + if (!isNonEmptyString(step.id)) return "Every step needs an id"; + if (seen.has(step.id)) return `Duplicate step id: ${step.id}`; + seen.add(step.id); + if (!STEP_TYPES.has(step.type)) { + return `Unknown step type: ${step.type}`; + } + + if (step.type === "if") { + if (!step.condition || !OPERATORS.has(step.condition.operator)) { + return "Condition needs a valid operator"; + } + const thenError = validateSteps(step.then ?? [], seen); + if (thenError) return thenError; + const elseError = validateSteps(step.else ?? [], seen); + if (elseError) return elseError; + } + + if (step.type === "http" && !isNonEmptyString(step.url)) { + return "HTTP steps need a URL"; + } + if (step.type === "run_command" && !isNonEmptyString(step.command)) { + return "Command steps need a command"; + } + if (step.type === "wait" && typeof step.seconds !== "number") { + return "Wait steps need a number of seconds"; + } + if (step.type === "set_var" && !isNonEmptyString(step.name)) { + return "Variable steps need a name"; + } + } + return null; +} + +/** Never leak a webhook token hash to the client. */ +function serialize(row: AutomationRow) { + let definition: AutomationDefinition | null = null; + try { + definition = JSON.parse(row.definition) as AutomationDefinition; + } catch { + definition = null; + } + + if (definition?.trigger?.kind === "webhook") { + definition = { + ...definition, + trigger: { ...definition.trigger, tokenHash: "" }, + }; + } + + return { ...row, definition }; +} + +async function syncSchedule( + automationId: number, + definition: AutomationDefinition, +): Promise { + const repository = createCurrentAutomationRepository(); + if (definition.trigger?.kind !== "schedule") { + await repository.deleteSchedule(automationId); + return; + } + + const trigger = definition.trigger; + await repository.upsertSchedule({ + automationId, + cron: trigger.cron ?? null, + intervalSeconds: trigger.intervalSeconds ?? null, + timezone: trigger.timezone ?? null, + nextDueAt: computeNextDueAt({ + cron: trigger.cron, + intervalSeconds: trigger.intervalSeconds, + timezone: trigger.timezone, + }), + }); +} + +/** + * @openapi + * /automations: + * get: + * summary: List the current user's automations + * description: Returns every automation the caller owns, with its parsed definition and linked notification channels. + * tags: + * - Automations + * responses: + * 200: + * description: List of automations. + * 403: + * description: Missing the automations.view permission. + */ +router.get( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const rows = await createCurrentAutomationRepository().list(userId); + res.json(rows.map(serialize)); + } catch (error) { + databaseLogger.error("Failed to list automations", error, { + operation: "automation_list_error", + userId, + }); + res.status(500).json({ error: "Failed to list automations" }); + } + }, +); + +/** + * @openapi + * /automations/{id}: + * get: + * summary: Fetch a single automation + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The automation. + * 404: + * description: Automation not found. + */ +router.get( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); + + try { + const row = await createCurrentAutomationRepository().findForUser( + id, + userId, + ); + if (!row) return res.status(404).json({ error: "Automation not found" }); + res.json(serialize(row)); + } catch (error) { + databaseLogger.error("Failed to fetch automation", error, { + operation: "automation_get_error", + userId, + }); + res.status(500).json({ error: "Failed to fetch automation" }); + } + }, +); + +/** + * @openapi + * /automations: + * post: + * summary: Create an automation + * description: Validates the trigger and every step before storing the definition. A schedule trigger also registers its next due time. + * tags: + * - Automations + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * definition: + * type: object + * responses: + * 201: + * description: The created automation. + * 400: + * description: Validation failed. + */ +router.post( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { name, description, enabled, definition, concurrencyPolicy } = + req.body ?? {}; + + if (!isNonEmptyString(name)) { + return res.status(400).json({ error: "Name is required" }); + } + + const validated = validateDefinition(definition); + if (!validated.ok || !validated.definition) { + return res.status(400).json({ error: validated.error }); + } + + // A webhook trigger's token is shown once here and only stored hashed. + let webhookToken: string | undefined; + if (validated.definition.trigger.kind === "webhook") { + webhookToken = crypto.randomBytes(32).toString("hex"); + validated.definition = { + ...validated.definition, + trigger: { + kind: "webhook", + tokenHash: hashToken(webhookToken), + }, + }; + } + + try { + const repository = createCurrentAutomationRepository(); + const created = await repository.create({ + userId, + name: name.trim(), + description: description ?? null, + enabled: enabled !== false, + definition: JSON.stringify(validated.definition), + concurrencyPolicy, + channels: Array.isArray(req.body?.channels) ? req.body.channels : [], + }); + + await syncSchedule(created.id, validated.definition); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "create_automation", + resourceType: "automation", + resourceId: String(created.id), + resourceName: created.name, + ipAddress, + userAgent, + success: true, + }); + + res.status(201).json({ ...serialize(created), webhookToken }); + } catch (error) { + databaseLogger.error("Failed to create automation", error, { + operation: "automation_create_error", + userId, + }); + res.status(500).json({ error: "Failed to create automation" }); + } + }, +); + +/** + * @openapi + * /automations/{id}: + * put: + * summary: Update an automation + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The updated automation. + * 400: + * description: Validation failed. + * 404: + * description: Automation not found. + */ +router.put( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); + + const repository = createCurrentAutomationRepository(); + const existing = await repository.findForUser(id, userId); + if (!existing) { + return res.status(404).json({ error: "Automation not found" }); + } + + const update: Record = {}; + if (req.body?.name !== undefined) { + if (!isNonEmptyString(req.body.name)) { + return res.status(400).json({ error: "Name cannot be empty" }); + } + update.name = req.body.name.trim(); + } + if (req.body?.description !== undefined) { + update.description = req.body.description; + } + if (req.body?.enabled !== undefined) update.enabled = !!req.body.enabled; + if (req.body?.concurrencyPolicy !== undefined) { + update.concurrencyPolicy = req.body.concurrencyPolicy; + } + if (Array.isArray(req.body?.channels)) update.channels = req.body.channels; + + let parsedDefinition: AutomationDefinition | null = null; + if (req.body?.definition !== undefined) { + const validated = validateDefinition(req.body.definition); + if (!validated.ok || !validated.definition) { + return res.status(400).json({ error: validated.error }); + } + + // Keep the stored token hash: the raw token is only ever shown once. + if (validated.definition.trigger.kind === "webhook") { + const previous = JSON.parse( + existing.definition, + ) as AutomationDefinition; + const previousHash = + previous.trigger?.kind === "webhook" + ? previous.trigger.tokenHash + : ""; + validated.definition = { + ...validated.definition, + trigger: { kind: "webhook", tokenHash: previousHash }, + }; + } + + parsedDefinition = validated.definition; + update.definition = JSON.stringify(validated.definition); + } + + try { + const updated = await repository.update(id, userId, update); + if (!updated) { + return res.status(404).json({ error: "Automation not found" }); + } + + if (parsedDefinition) await syncSchedule(id, parsedDefinition); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "update_automation", + resourceType: "automation", + resourceId: String(id), + resourceName: updated.name, + ipAddress, + userAgent, + success: true, + }); + + res.json(serialize(updated)); + } catch (error) { + databaseLogger.error("Failed to update automation", error, { + operation: "automation_update_error", + userId, + }); + res.status(500).json({ error: "Failed to update automation" }); + } + }, +); + +/** + * @openapi + * /automations/{id}: + * delete: + * summary: Delete an automation + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Deleted. + * 404: + * description: Automation not found. + */ +router.delete( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); + + try { + const deleted = await createCurrentAutomationRepository().delete( + id, + userId, + ); + if (!deleted) { + return res.status(404).json({ error: "Automation not found" }); + } + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "delete_automation", + resourceType: "automation", + resourceId: String(id), + ipAddress, + userAgent, + success: true, + }); + + res.json({ success: true }); + } catch (error) { + databaseLogger.error("Failed to delete automation", error, { + operation: "automation_delete_error", + userId, + }); + res.status(500).json({ error: "Failed to delete automation" }); + } + }, +); + +/** + * @openapi + * /automations/{id}/run: + * post: + * summary: Run an automation now + * description: Runs immediately, as the automation's owner. Pass dryRun to record what each step would do without touching anything outside Termix. + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * dryRun: + * type: boolean + * responses: + * 200: + * description: The run outcome. + * 404: + * description: Automation not found. + */ +router.post( + "/:id/run", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); + + const existing = await createCurrentAutomationRepository().findForUser( + id, + userId, + ); + if (!existing) { + return res.status(404).json({ error: "Automation not found" }); + } + + try { + const outcome = await AutomationEngine.getInstance().run({ + automationId: id, + triggerType: "manual", + triggerContext: { manual: true, requestedBy: userId }, + dryRun: req.body?.dryRun === true, + }); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "run_automation", + resourceType: "automation", + resourceId: String(id), + resourceName: existing.name, + details: JSON.stringify({ + status: outcome.status, + dryRun: req.body?.dryRun === true, + }), + ipAddress, + userAgent, + success: outcome.status === "success", + }); + + res.json(outcome); + } catch (error) { + databaseLogger.error("Failed to run automation", error, { + operation: "automation_run_error", + userId, + }); + res.status(500).json({ error: "Failed to run automation" }); + } + }, +); + +/** + * @openapi + * /automations/runs: + * get: + * summary: List automation runs + * tags: + * - Automations + * parameters: + * - in: query + * name: automationId + * schema: + * type: integer + * - in: query + * name: limit + * schema: + * type: integer + * responses: + * 200: + * description: Recent runs, newest first. + */ +router.get( + "/runs/history", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const runs = await createCurrentAutomationRepository().listRuns(userId, { + automationId: parseId(req.query.automationId) ?? undefined, + limit: Number(req.query.limit) || 50, + offset: Number(req.query.offset) || 0, + }); + res.json(runs); + } catch (error) { + databaseLogger.error("Failed to list automation runs", error, { + operation: "automation_runs_error", + userId, + }); + res.status(500).json({ error: "Failed to list runs" }); + } + }, +); + +/** + * @openapi + * /automations/runs/{runId}/steps: + * get: + * summary: Step-by-step results for a run + * tags: + * - Automations + * parameters: + * - in: path + * name: runId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The run's steps in order. + * 404: + * description: Run not found. + */ +router.get( + "/runs/:runId/steps", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const runId = parseId(req.params.runId); + if (runId === null) return res.status(400).json({ error: "Invalid id" }); + + try { + const repository = createCurrentAutomationRepository(); + const run = await repository.findRunForUser(runId, userId); + if (!run) return res.status(404).json({ error: "Run not found" }); + + res.json(await repository.listRunSteps(runId)); + } catch (error) { + databaseLogger.error("Failed to list run steps", error, { + operation: "automation_run_steps_error", + userId, + }); + res.status(500).json({ error: "Failed to list run steps" }); + } + }, +); + +/** + * @openapi + * /automations/webhook/{token}: + * post: + * summary: Trigger an automation from an external system + * description: Unauthenticated by design; the 32-byte token in the path is the credential and is compared against a stored hash in constant time. + * tags: + * - Automations + * parameters: + * - in: path + * name: token + * required: true + * schema: + * type: string + * responses: + * 202: + * description: The run was accepted. + * 404: + * description: No automation matches that token. + */ +router.post("/webhook/:token", async (req: Request, res: Response) => { + const token = req.params.token; + if (!isNonEmptyString(token) || token.length < 32) { + return res.status(404).json({ error: "Not found" }); + } + + try { + const repository = createCurrentAutomationRepository(); + const candidates = await repository.listAllEnabled(); + const wanted = hashToken(token); + + const match = candidates.find((row) => { + try { + const definition = JSON.parse(row.definition) as AutomationDefinition; + if (definition.trigger?.kind !== "webhook") return false; + return timingSafeEqual(definition.trigger.tokenHash, wanted); + } catch { + return false; + } + }); + + if (!match) return res.status(404).json({ error: "Not found" }); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: match.id, + triggerType: "webhook", + triggerContext: { + body: req.body ?? {}, + receivedAt: new Date().toISOString(), + }, + }); + + res.status(202).json({ runId: outcome.runId, status: outcome.status }); + } catch (error) { + databaseLogger.error("Webhook automation failed", error, { + operation: "automation_webhook_error", + }); + res.status(500).json({ error: "Failed to run automation" }); + } +}); + +function hashToken(token: string): string { + return crypto.createHash("sha256").update(token).digest("hex"); +} + +function timingSafeEqual(a: string, b: string): boolean { + const left = Buffer.from(a || "", "utf8"); + const right = Buffer.from(b || "", "utf8"); + if (left.length !== right.length) return false; + return crypto.timingSafeEqual(left, right); +} + +export default router; diff --git a/src/backend/database/routes/c2s-tunnel-presets.ts b/src/backend/database/routes/c2s-tunnel-presets.ts index f11d5da..e3b8c25 100644 --- a/src/backend/database/routes/c2s-tunnel-presets.ts +++ b/src/backend/database/routes/c2s-tunnel-presets.ts @@ -2,8 +2,7 @@ import type { AuthenticatedRequest, TunnelConnection, } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import type { C2sTunnelPresetRecord } from "../repositories/c2s-tunnel-preset-repository.js"; diff --git a/src/backend/database/routes/credential-bulk-routes.ts b/src/backend/database/routes/credential-bulk-routes.ts new file mode 100644 index 0000000..00583d5 --- /dev/null +++ b/src/backend/database/routes/credential-bulk-routes.ts @@ -0,0 +1,88 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; +import type { Request, RequestHandler, Response, Router } from "express"; +import { authLogger } from "../../utils/logger.js"; +import { createCurrentCredentialRepository } from "../repositories/factory.js"; + +export function registerCredentialBulkRoutes( + router: Router, + authenticateJWT: RequestHandler, +): void { + /** + * @openapi + * /credentials/reorder: + * put: + * summary: Reorder credentials + * description: Sets a manual sortOrder for multiple credentials within the same folder, used by drag-to-reorder in the sidebar's manual sort mode. + * tags: + * - Credentials + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * positions: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * sortOrder: + * type: integer + * responses: + * 200: + * description: Credentials reordered successfully. + * 400: + * description: Invalid positions array. + * 500: + * description: Failed to reorder credentials. + */ + router.put( + "/reorder", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { positions } = req.body as { + positions?: { id?: unknown; sortOrder?: unknown }[]; + }; + + if (!Array.isArray(positions)) { + return res.status(400).json({ error: "positions array is required" }); + } + + const normalized: { id: number; sortOrder: number }[] = []; + for (const entry of positions) { + if ( + typeof entry?.id !== "number" || + !Number.isInteger(entry.id) || + typeof entry.sortOrder !== "number" || + !Number.isFinite(entry.sortOrder) + ) { + return res.status(400).json({ + error: + "Each position requires an integer id and a numeric sortOrder", + }); + } + normalized.push({ id: entry.id, sortOrder: entry.sortOrder }); + } + + if (normalized.length === 0) { + return res.status(400).json({ error: "positions array is required" }); + } + + try { + const updated = + await createCurrentCredentialRepository().reorderForUser( + userId, + normalized, + ); + return res.json({ updated }); + } catch (error) { + authLogger.error("Failed to reorder credentials:", error); + return res.status(500).json({ error: "Failed to reorder credentials" }); + } + }, + ); +} diff --git a/src/backend/database/routes/credential-deploy-routes.ts b/src/backend/database/routes/credential-deploy-routes.ts index 1e85386..fdf9864 100644 --- a/src/backend/database/routes/credential-deploy-routes.ts +++ b/src/backend/database/routes/credential-deploy-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest, CredentialBackend, @@ -234,7 +235,7 @@ async function deploySSHKeyToHost( conn.end(); resolve({ success: false, - error: error instanceof Error ? error.message : "Deployment failed", + error: getErrorMessage(error, "Deployment failed"), }); } }); @@ -331,7 +332,7 @@ async function deploySSHKeyToHost( clearTimeout(connectionTimeout); resolve({ success: false, - error: `Invalid SSH key format: ${keyError instanceof Error ? keyError.message : "Unknown error"}`, + error: `Invalid SSH key format: ${getErrorMessage(keyError)}`, }); return; } @@ -349,7 +350,7 @@ async function deploySSHKeyToHost( clearTimeout(connectionTimeout); resolve({ success: false, - error: error instanceof Error ? error.message : "Connection failed", + error: getErrorMessage(error, "Connection failed"), }); } }); @@ -527,8 +528,7 @@ export function registerCredentialDeployRoutes( } catch (error) { res.status(500).json({ success: false, - error: - error instanceof Error ? error.message : "Failed to deploy SSH key", + error: getErrorMessage(error, "Failed to deploy SSH key"), }); } }, diff --git a/src/backend/database/routes/credential-key-routes.ts b/src/backend/database/routes/credential-key-routes.ts index 3ff5496..0d91278 100644 --- a/src/backend/database/routes/credential-key-routes.ts +++ b/src/backend/database/routes/credential-key-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { Request, RequestHandler, Response, Router } from "express"; import crypto from "crypto"; import ssh2Pkg from "ssh2"; @@ -54,8 +55,7 @@ function generateSSHKeyPair( } catch (error) { return { success: false, - error: - error instanceof Error ? error.message : "SSH key generation failed", + error: getErrorMessage(error, "SSH key generation failed"), }; } } @@ -116,10 +116,7 @@ export function registerCredentialKeyRoutes( } catch (error) { authLogger.error("Failed to detect key type", error); res.status(500).json({ - error: - error instanceof Error - ? error.message - : "Failed to detect key type", + error: getErrorMessage(error, "Failed to detect key type"), }); } }, @@ -174,10 +171,7 @@ export function registerCredentialKeyRoutes( } catch (error) { authLogger.error("Failed to detect public key type", error); res.status(500).json({ - error: - error instanceof Error - ? error.message - : "Failed to detect public key type", + error: getErrorMessage(error, "Failed to detect public key type"), }); } }, @@ -245,10 +239,7 @@ export function registerCredentialKeyRoutes( } catch (error) { authLogger.error("Failed to validate key pair", error); res.status(500).json({ - error: - error instanceof Error - ? error.message - : "Failed to validate key pair", + error: getErrorMessage(error, "Failed to validate key pair"), }); } }, @@ -312,10 +303,7 @@ export function registerCredentialKeyRoutes( authLogger.error("Failed to generate key pair", error); res.status(500).json({ success: false, - error: - error instanceof Error - ? error.message - : "Failed to generate key pair", + error: getErrorMessage(error, "Failed to generate key pair"), }); } }, @@ -501,10 +489,7 @@ export function registerCredentialKeyRoutes( authLogger.error("Failed to generate public key", error); res.status(500).json({ success: false, - error: - error instanceof Error - ? error.message - : "Failed to generate public key", + error: getErrorMessage(error, "Failed to generate public key"), }); } }, diff --git a/src/backend/database/routes/credential-sidebar-preferences.ts b/src/backend/database/routes/credential-sidebar-preferences.ts new file mode 100644 index 0000000..cca086f --- /dev/null +++ b/src/backend/database/routes/credential-sidebar-preferences.ts @@ -0,0 +1,115 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; +import express, { type Request, type Response } from "express"; +import { databaseLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { createCurrentCredentialSidebarPreferenceRepository } from "../repositories/factory.js"; +import { + defaultCredentialSidebarPreferences, + sanitizeCredentialSidebarPreferences, +} from "../../../types/credential-sidebar-preferences.js"; + +const router = express.Router(); +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); + +/** + * @openapi + * /credential-sidebar/preferences: + * get: + * summary: Get the credential sidebar preferences for the current user + * description: Returns the current user's saved credential sidebar preferences (sort, filters, open folders, display settings). Unlike /host-sidebar/preferences, there is no legacy-column migration to perform here โ€” credentials never had exploded preference columns on userPreferences โ€” so a first-time GET simply returns the defaults without writing a row; a row is only created once the user actually changes something via PUT. + * tags: + * - Credential Sidebar + * responses: + * 200: + * description: The current user's credential sidebar preferences. + */ +router.get("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const existing = + await createCurrentCredentialSidebarPreferenceRepository().findByUserId( + userId, + ); + + if (existing) { + const preferences = sanitizeCredentialSidebarPreferences( + JSON.parse(existing.data), + ); + return res.json({ preferences }); + } + + return res.json({ preferences: defaultCredentialSidebarPreferences() }); + } catch (e) { + databaseLogger.error("Failed to get credential sidebar preferences", e, { + operation: "get_credential_sidebar_preferences", + userId, + }); + return res + .status(500) + .json({ error: "Failed to get credential sidebar preferences" }); + } +}); + +/** + * @openapi + * /credential-sidebar/preferences: + * put: + * summary: Update the credential sidebar preferences for the current user + * description: Persists the current user's credential sidebar preferences (sort, filters, open folders, display settings) as a single JSON document. + * tags: + * - Credential Sidebar + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: Preferences updated successfully. + * 400: + * description: Invalid preferences payload. + */ +router.put("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + + if (!req.body || typeof req.body !== "object") { + return res.status(400).json({ error: "Invalid preferences payload" }); + } + + try { + const existing = + await createCurrentCredentialSidebarPreferenceRepository().findByUserId( + userId, + ); + const base = existing + ? sanitizeCredentialSidebarPreferences(JSON.parse(existing.data)) + : defaultCredentialSidebarPreferences(); + + const merged = sanitizeCredentialSidebarPreferences({ + ...base, + ...req.body, + display: { ...base.display, ...(req.body.display ?? {}) }, + sort: { ...base.sort, ...(req.body.sort ?? {}) }, + filters: { ...base.filters, ...(req.body.filters ?? {}) }, + }); + + await createCurrentCredentialSidebarPreferenceRepository().upsert( + userId, + JSON.stringify(merged), + ); + + return res.json({ success: true, preferences: merged }); + } catch (e) { + databaseLogger.error("Failed to update credential sidebar preferences", e, { + operation: "update_credential_sidebar_preferences", + userId, + }); + return res + .status(500) + .json({ error: "Failed to update credential sidebar preferences" }); + } +}); + +export default router; diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index a467426..0a76164 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -1,17 +1,22 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { parseSSHKey } from "../../utils/ssh-key-utils.js"; import { registerCredentialKeyRoutes } from "./credential-key-routes.js"; import { registerCredentialDeployRoutes } from "./credential-deploy-routes.js"; -import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; +import { registerCredentialBulkRoutes } from "./credential-bulk-routes.js"; +import { + logAudit, + getAuditUsername, + getRequestMeta, +} from "../../utils/audit-logger.js"; import { createCurrentCredentialRepository, createCurrentHostResolutionRepository, createCurrentHostRepository, - createCurrentUserRepository, + createCurrentSyncTombstoneRepository, } from "../repositories/factory.js"; const router = express.Router(); @@ -24,11 +29,6 @@ const authManager = AuthManager.getInstance(); const authenticateJWT = authManager.createAuthMiddleware(); const requireDataAccess = authManager.createDataAccessMiddleware(); -async function getAuditUsername(userId: string): Promise { - const actor = await createCurrentUserRepository().findById(userId); - return actor?.username ?? userId; -} - /** * @openapi * /credentials: @@ -225,8 +225,7 @@ router.post( username, }); res.status(500).json({ - error: - err instanceof Error ? err.message : "Failed to create credential", + error: getErrorMessage(err, "Failed to create credential"), }); } }, @@ -309,6 +308,11 @@ router.get( }, ); +// Registered here (before the PUT /:id route below) so the literal +// "/reorder" path segment is matched before Express falls through to the +// PUT /:id param route and treats "reorder" as an id. +registerCredentialBulkRoutes(router, authenticateJWT); + /** * @openapi * /credentials/{id}: @@ -378,8 +382,7 @@ router.get( } catch (err) { authLogger.error("Failed to fetch credential", err); res.status(500).json({ - error: - err instanceof Error ? err.message : "Failed to fetch credential", + error: getErrorMessage(err, "Failed to fetch credential"), }); } }, @@ -552,8 +555,7 @@ router.put( } catch (err) { authLogger.error("Failed to update credential", err); res.status(500).json({ - error: - err instanceof Error ? err.message : "Failed to update credential", + error: getErrorMessage(err, "Failed to update credential"), }); } }, @@ -642,6 +644,13 @@ router.delete( userId, credentialId, ); + if (credentialToDelete.syncId) { + await createCurrentSyncTombstoneRepository().record( + userId, + "sshCredentials", + credentialToDelete.syncId, + ); + } // Shares stay in place; re-snapshot so recipients fall back to whatever // auth the host still has (or lose the stale credential copy). @@ -672,8 +681,7 @@ router.delete( } catch (err) { authLogger.error("Failed to delete credential", err); res.status(500).json({ - error: - err instanceof Error ? err.message : "Failed to delete credential", + error: getErrorMessage(err, "Failed to delete credential"), }); } }, @@ -760,10 +768,7 @@ router.post( } catch (err) { authLogger.error("Failed to apply credential to host", err); res.status(500).json({ - error: - err instanceof Error - ? err.message - : "Failed to apply credential to host", + error: getErrorMessage(err, "Failed to apply credential to host"), }); } }, @@ -816,10 +821,7 @@ router.get( } catch (err) { authLogger.error("Failed to fetch hosts using credential", err); res.status(500).json({ - error: - err instanceof Error - ? err.message - : "Failed to fetch hosts using credential", + error: getErrorMessage(err, "Failed to fetch hosts using credential"), }); } }, @@ -839,6 +841,8 @@ function formatCredentialOutput( ? credential.tags.split(",").filter(Boolean) : [] : [], + pin: !!credential.pin, + sortOrder: credential.sortOrder ?? null, authType: credential.authType, username: credential.username || null, publicKey: credential.publicKey, diff --git a/src/backend/database/routes/dashboard-service-links-routes.ts b/src/backend/database/routes/dashboard-service-links-routes.ts index 3482935..6b9dc2b 100644 --- a/src/backend/database/routes/dashboard-service-links-routes.ts +++ b/src/backend/database/routes/dashboard-service-links-routes.ts @@ -1,10 +1,12 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { dashboardLogger } from "../../utils/logger.js"; import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; import { isNonEmptyString } from "./host-normalizers.js"; -import express from "express"; -import { createCurrentDashboardServiceLinkRepository } from "../repositories/factory.js"; +import { + createCurrentDashboardServiceLinkRepository, + createCurrentSyncTombstoneRepository, +} from "../repositories/factory.js"; export const dashboardServiceLinksRouter = express.Router(); @@ -152,10 +154,18 @@ dashboardServiceLinksRouter.delete( return res.status(404).json({ error: "Not found" }); } - await createCurrentDashboardServiceLinkRepository().deleteForUser( - userId, - id, - ); + const deleted = + await createCurrentDashboardServiceLinkRepository().deleteForUser( + userId, + id, + ); + if (deleted?.syncId) { + await createCurrentSyncTombstoneRepository().record( + userId, + "dashboardServiceLinks", + deleted.syncId, + ); + } DatabaseSaveTrigger.triggerSave("dashboard_service_link_deleted"); res.json({ message: "Service link deleted" }); diff --git a/src/backend/database/routes/delete-user-data.ts b/src/backend/database/routes/delete-user-data.ts index b3ec33e..3c54983 100644 --- a/src/backend/database/routes/delete-user-data.ts +++ b/src/backend/database/routes/delete-user-data.ts @@ -1,5 +1,6 @@ import { authLogger } from "../../utils/logger.js"; import { + createCurrentAiRepository, createCurrentAlertRepository, createCurrentApiKeyRepository, createCurrentAuditLogRepository, @@ -15,6 +16,9 @@ import { createCurrentHostFolderRepository, createCurrentHostMetricsPreferenceRepository, createCurrentHostRepository, + createCurrentHostSidebarPreferenceRepository, + createCurrentCredentialSidebarPreferenceRepository, + createCurrentUiPreferenceRepository, createCurrentNetworkTopologyRepository, createCurrentOpksshTokenRepository, createCurrentOpenTabRepository, @@ -44,7 +48,9 @@ export async function deleteUserAndRelatedData(userId: string): Promise { userId, ); - await createCurrentSessionRecordingRepository().deleteByUserId(userId); + // Retained rather than deleted: these outlive the account by design. + // See anonymizeByUserId on each repository. + await createCurrentSessionRecordingRepository().anonymizeByUserId(userId); await createCurrentRbacAccessRepository().deleteHostAccessForUserReferences( userId, @@ -55,8 +61,9 @@ export async function deleteUserAndRelatedData(userId: string): Promise { await createCurrentTrustedDeviceRepository().deleteByUserId(userId); await createCurrentRoleRepository().removeAllRolesFromUser(userId); + await createCurrentAiRepository().deleteByUserId(userId); await createCurrentAlertRepository().deleteByUserId(userId); - await createCurrentAuditLogRepository().deleteByUserId(userId); + await createCurrentAuditLogRepository().anonymizeByUserId(userId); await createCurrentSshCredentialUsageRepository().deleteByUserId(userId); @@ -75,6 +82,11 @@ export async function deleteUserAndRelatedData(userId: string): Promise { await createCurrentHostHealthRepository().deleteByUserId(userId); await createCurrentHostMetricsPreferenceRepository().deleteByUserId(userId); + await createCurrentHostSidebarPreferenceRepository().deleteByUserId(userId); + await createCurrentCredentialSidebarPreferenceRepository().deleteByUserId( + userId, + ); + await createCurrentUiPreferenceRepository().deleteByUserId(userId); await createCurrentHostRepository().deleteByUserId(userId); await createCurrentCredentialRepository().deleteByUserId(userId); diff --git a/src/backend/database/routes/desktop-auto-session.ts b/src/backend/database/routes/desktop-auto-session.ts new file mode 100644 index 0000000..d1ffa95 --- /dev/null +++ b/src/backend/database/routes/desktop-auto-session.ts @@ -0,0 +1,64 @@ +import type { Request } from "express"; +import type { UserRecord } from "../repositories/user-repository.js"; + +export function isLoopbackRequest(req: Request): boolean { + // Requests relayed by the bundled nginx always carry X-Real-IP, which + // nginx overwrites with the actual client address -- so its presence + // means the caller reached the backend through the reverse proxy and is + // not a local process, whatever the TCP peer address says (it is nginx + // itself on loopback). Everything else is judged by the TCP peer + // address, which no client-supplied header can influence: with + // `trust proxy = true`, req.ip comes from X-Forwarded-For and would let + // any remote caller claim to be loopback. + if (req.headers["x-real-ip"]) return false; + + const ip = req.socket?.remoteAddress || ""; + return ( + ip === "127.0.0.1" || + ip === "::1" || + ip === "::ffff:127.0.0.1" || + ip.endsWith(":127.0.0.1") + ); +} + +export function extractBearerOrCookieToken(req: Request): string | undefined { + const cookieToken = (req as Request & { cookies?: Record }) + .cookies?.jwt; + if (cookieToken) return cookieToken; + + const authHeader = req.headers["authorization"]; + if (authHeader?.startsWith("Bearer ")) { + return authHeader.slice("Bearer ".length); + } + return undefined; +} + +/** + * Decides who the desktop auto-session endpoint should silently log in as. + * + * The local embedded backend's trust boundary is machine access (loopback), + * not any individual account's credentials -- anyone who can reach loopback + * already has full filesystem access to the local, encrypted-at-rest + * database and its keys. A login form must never appear for the local + * backend, under any circumstance, including a local database that ended + * up with more than one user (e.g. from repeated manual registration + * during testing, or a household sharing one install) -- a user in that + * state deserves to get into the app they installed, not a confusing, + * unexplained dead end. So this always returns a single, deterministic + * user: the admin account if one exists, else the earliest-registered + * account. It never returns null. + */ +export function resolveDesktopAutoSessionUser( + allUsers: UserRecord[], +): UserRecord | null { + if (allUsers.length === 0) return null; + if (allUsers.length === 1) return allUsers[0]; + + const admin = allUsers.find((user) => user.isAdmin); + if (admin) return admin; + + return [...allUsers].sort( + (a, b) => + new Date(a.registeredAt).getTime() - new Date(b.registeredAt).getTime(), + )[0]; +} diff --git a/src/backend/database/routes/fleet-routes.ts b/src/backend/database/routes/fleet-routes.ts new file mode 100644 index 0000000..568e59c --- /dev/null +++ b/src/backend/database/routes/fleet-routes.ts @@ -0,0 +1,1499 @@ +import { getErrorMessage } from "../../utils/error-message.js"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import express, { type Request, type Response } from "express"; +import multer from "multer"; +import JSZip from "jszip"; +import type { Client, SFTPWrapper } from "ssh2"; +import { authLogger, databaseLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { + PermissionManager, + type HostAction, +} from "../../utils/permission-manager.js"; +import { + isSharePermissionLevel, + expiryFromDuration, + parseShareTargets, +} from "./rbac.js"; +import { + createCurrentFleetRepository, + createCurrentFleetInventoryRepository, + createCurrentRbacAccessRepository, + createCurrentRoleRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; +import { resolveHostById } from "../../hosts/host-resolver.js"; +import { + getFleetPoolKey, + createFleetSshFactory, +} from "../../hosts/ssh-client-factory.js"; +import { withConnection } from "../../hosts/ssh-connection-pool.js"; +import { execCommand } from "../../hosts/metrics/widgets/common-utils.js"; +import { detectPlatform } from "../../hosts/metrics/managers/platform.js"; +import { + execElevated, + ElevationError, +} from "../../hosts/metrics/managers/exec-elevated.js"; +import { buildPackageActionCommand } from "../../hosts/metrics/managers/packages.js"; +import { isValidPackageName } from "../../hosts/metrics/managers/validation.js"; +import { resolveSnippetCommand } from "./snippets-execution.js"; + +const router = express.Router(); + +const authManager = AuthManager.getInstance(); +const permissionManager = PermissionManager.getInstance(); + +const authenticateJWT = authManager.createAuthMiddleware(); +const requireDataAccess = authManager.createDataAccessMiddleware(); + +const FLEET_TRANSFER_MAX_BYTES = 200 * 1024 * 1024; +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: FLEET_TRANSFER_MAX_BYTES }, +}); + +function isNonEmptyString(val: unknown): val is string { + return typeof val === "string" && val.trim().length > 0; +} + +function parseFleetId(raw: unknown): number | null { + const id = typeof raw === "string" ? parseInt(raw, 10) : NaN; + return Number.isInteger(id) ? id : null; +} + +/** + * @openapi + * /fleets: + * get: + * summary: List the current user's fleets + * tags: + * - Fleets + * responses: + * 200: + * description: List of fleets with member counts. + */ +router.get( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + + try { + const fleetRepository = createCurrentFleetRepository(); + const fleetList = await fleetRepository.listByUser(userId); + + const withCounts = await Promise.all( + fleetList.map(async (fleet) => { + const members = await fleetRepository.listEffectiveMembers( + userId, + fleet.id, + ); + return { + ...fleet, + tagRules: fleet.tagRules ? JSON.parse(fleet.tagRules) : [], + memberCount: members.length, + }; + }), + ); + + res.json(withCounts); + } catch (err) { + databaseLogger.error("Failed to list fleets", err, { + operation: "fleet_list_failed", + userId, + }); + res.status(500).json({ error: "Failed to list fleets" }); + } + }, +); + +/** + * @openapi + * /fleets: + * post: + * summary: Create a fleet + * tags: + * - Fleets + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: + * type: string + * color: + * type: string + * icon: + * type: string + * tagRules: + * type: array + * items: + * type: string + * responses: + * 200: + * description: Fleet created. + * 400: + * description: Invalid request body. + */ +router.post( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { name, description, color, icon, tagRules } = req.body ?? {}; + + if (!isNonEmptyString(name)) { + return res.status(400).json({ error: "Fleet name is required" }); + } + + if ( + tagRules !== undefined && + (!Array.isArray(tagRules) || tagRules.some((t) => typeof t !== "string")) + ) { + return res + .status(400) + .json({ error: "tagRules must be an array of strings" }); + } + + try { + const fleet = await createCurrentFleetRepository().create(userId, { + name: name.trim(), + description: isNonEmptyString(description) ? description.trim() : null, + color: isNonEmptyString(color) ? color : null, + icon: isNonEmptyString(icon) ? icon : null, + tagRules, + }); + + authLogger.success(`Fleet created: ${fleet.name}`, { + operation: "fleet_create_success", + userId, + fleetId: fleet.id, + }); + + res.json({ ...fleet, tagRules: tagRules ?? [] }); + } catch (err) { + databaseLogger.error("Failed to create fleet", err, { + operation: "fleet_create_failed", + userId, + }); + res.status(500).json({ error: "Failed to create fleet" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}: + * patch: + * summary: Update a fleet + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Fleet updated. + * 404: + * description: Fleet not found. + */ +router.patch( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + + const { name, description, color, icon, tagRules } = req.body ?? {}; + + if (name !== undefined && !isNonEmptyString(name)) { + return res.status(400).json({ error: "Fleet name cannot be empty" }); + } + + if ( + tagRules !== undefined && + (!Array.isArray(tagRules) || tagRules.some((t) => typeof t !== "string")) + ) { + return res + .status(400) + .json({ error: "tagRules must be an array of strings" }); + } + + try { + const updated = await createCurrentFleetRepository().update( + userId, + fleetId, + { + name: name !== undefined ? name.trim() : undefined, + description, + color, + icon, + tagRules, + }, + ); + + if (!updated) { + return res.status(404).json({ error: "Fleet not found" }); + } + + res.json({ + ...updated, + tagRules: updated.tagRules ? JSON.parse(updated.tagRules) : [], + }); + } catch (err) { + databaseLogger.error("Failed to update fleet", err, { + operation: "fleet_update_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to update fleet" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}: + * delete: + * summary: Delete a fleet + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Fleet deleted. + * 404: + * description: Fleet not found. + */ +router.delete( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + + try { + const deleted = await createCurrentFleetRepository().delete( + userId, + fleetId, + ); + if (!deleted) { + return res.status(404).json({ error: "Fleet not found" }); + } + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to delete fleet", err, { + operation: "fleet_delete_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to delete fleet" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/members: + * get: + * summary: List the resolved effective members of a fleet + * description: Returns the union of statically-added hosts and hosts matched by the fleet's tag rules, each annotated with the caller's permission level on that host. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Resolved member hosts. + * 404: + * description: Fleet not found. + */ +router.get( + "/:id/members", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + + try { + const fleetRepository = createCurrentFleetRepository(); + const fleet = await fleetRepository.findById(userId, fleetId); + if (!fleet) { + return res.status(404).json({ error: "Fleet not found" }); + } + + const staticIds = await fleetRepository.listStaticMemberIds(fleetId); + const staticIdSet = new Set(staticIds); + const members = await fleetRepository.listEffectiveMembers( + userId, + fleetId, + ); + + const annotated = await Promise.all( + members.map(async (host) => { + const access = await permissionManager.canAccessHost( + userId, + host.id, + "connect", + ); + return { + id: host.id, + name: host.name, + ip: host.ip, + tags: host.tags ? host.tags.split(",").filter(Boolean) : [], + static: staticIdSet.has(host.id), + permissionLevel: access.isOwner + ? "manage" + : (access.permissionLevel ?? null), + }; + }), + ); + + res.json(annotated); + } catch (err) { + databaseLogger.error("Failed to list fleet members", err, { + operation: "fleet_members_list_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to list fleet members" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/members: + * post: + * summary: Add a host to a fleet's static membership + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * hostId: + * type: number + * responses: + * 200: + * description: Host added. + * 404: + * description: Fleet or host not found. + */ +router.post( + "/:id/members", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + const hostId = Number(req.body?.hostId); + + if (fleetId === null || !Number.isInteger(hostId)) { + return res + .status(400) + .json({ error: "Valid fleetId and hostId are required" }); + } + + try { + const fleetRepository = createCurrentFleetRepository(); + const fleet = await fleetRepository.findById(userId, fleetId); + if (!fleet) { + return res.status(404).json({ error: "Fleet not found" }); + } + + const access = await permissionManager.canAccessHost( + userId, + hostId, + "connect", + ); + if (!access.hasAccess) { + return res.status(404).json({ error: "Host not found" }); + } + + await fleetRepository.addMember(fleetId, hostId); + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to add fleet member", err, { + operation: "fleet_member_add_failed", + userId, + fleetId, + hostId, + }); + res.status(500).json({ error: "Failed to add fleet member" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/members/{hostId}: + * delete: + * summary: Remove a host from a fleet's static membership + * description: Only removes the static membership row - a host still matched by the fleet's tag rules remains an effective member. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * - in: path + * name: hostId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Host removed. + * 404: + * description: Fleet or membership not found. + */ +router.delete( + "/:id/members/:hostId", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + const hostId = parseFleetId(req.params.hostId); + + if (fleetId === null || hostId === null) { + return res.status(400).json({ error: "Invalid fleet or host ID" }); + } + + try { + const fleetRepository = createCurrentFleetRepository(); + const fleet = await fleetRepository.findById(userId, fleetId); + if (!fleet) { + return res.status(404).json({ error: "Fleet not found" }); + } + + const removed = await fleetRepository.removeMember(fleetId, hostId); + if (!removed) { + return res.status(404).json({ error: "Membership not found" }); + } + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to remove fleet member", err, { + operation: "fleet_member_remove_failed", + userId, + fleetId, + hostId, + }); + res.status(500).json({ error: "Failed to remove fleet member" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/share: + * post: + * summary: Share a fleet's current member hosts with users or roles + * description: Snapshot at share time - grants hostAccess for every current member host to each target. Hosts added to the fleet later are not automatically shared; re-run this route to extend sharing to new members. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * targets: + * type: array + * items: + * type: object + * permissionLevel: + * type: string + * durationHours: + * type: number + * responses: + * 200: + * description: Fleet shared. + * 404: + * description: Fleet not found. + */ +router.post( + "/:id/share", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + + try { + const fleetRepository = createCurrentFleetRepository(); + const fleet = await fleetRepository.findById(userId, fleetId); + if (!fleet) { + return res.status(404).json({ error: "Fleet not found" }); + } + + const targets = parseShareTargets(req.body ?? {}); + if (!targets) { + return res.status(400).json({ + error: + "targets must be a non-empty array of { type: 'user'|'role', id } entries", + }); + } + + const { durationHours, permissionLevel = "connect" } = req.body; + + if (!isSharePermissionLevel(permissionLevel)) { + return res.status(400).json({ error: "Invalid permission level" }); + } + + const userRepository = createCurrentUserRepository(); + const roleRepository = createCurrentRoleRepository(); + for (const target of targets) { + if (target.type === "user") { + const targetUser = await userRepository.findById(target.id as string); + if (!targetUser) { + return res + .status(404) + .json({ error: "Target user not found", targetId: target.id }); + } + } else { + const targetRole = await roleRepository.findRoleById( + target.id as number, + ); + if (!targetRole) { + return res + .status(404) + .json({ error: "Target role not found", targetId: target.id }); + } + } + } + + const members = await fleetRepository.listEffectiveMembers( + userId, + fleetId, + ); + const expiresAt = expiryFromDuration(durationHours); + const rbacAccessRepository = createCurrentRbacAccessRepository(); + const { SharedHostSecretsManager } = + await import("../../utils/shared-host-secrets-manager.js"); + const secretsManager = SharedHostSecretsManager.getInstance(); + + const hostResults: Array<{ + hostId: number; + shared: boolean; + reason?: string; + }> = []; + + for (const host of members) { + if (targets.some((t) => t.type === "user" && t.id === host.userId)) { + hostResults.push({ hostId: host.id, shared: false, reason: "owner" }); + continue; + } + + const access = await permissionManager.canAccessHost( + userId, + host.id, + "manage", + ); + if (!access.hasAccess) { + hostResults.push({ + hostId: host.id, + shared: false, + reason: "forbidden", + }); + continue; + } + + for (const target of targets) { + const accessGrant = await rbacAccessRepository.upsertHostAccess({ + hostId: host.id, + grantedBy: userId, + permissionLevel, + expiresAt, + ...(target.type === "user" + ? { + targetType: "user" as const, + targetUserId: target.id as string, + } + : { + targetType: "role" as const, + targetRoleId: target.id as number, + }), + }); + + try { + if (target.type === "user") { + await secretsManager.snapshotForUser( + accessGrant.id, + host.id, + target.id as string, + host.userId, + ); + } else { + await secretsManager.snapshotForRole( + accessGrant.id, + host.id, + target.id as number, + host.userId, + ); + } + } catch (snapshotError) { + databaseLogger.warn("Fleet shared but secret snapshot failed", { + operation: "fleet_share_snapshot_failed", + hostId: host.id, + accessId: accessGrant.id, + error: getErrorMessage(snapshotError), + }); + } + } + + hostResults.push({ hostId: host.id, shared: true }); + } + + const sharedCount = hostResults.filter((r) => r.shared).length; + + databaseLogger.success("Fleet shared successfully", { + operation: "fleet_share_success", + userId, + fleetId, + hostsShared: sharedCount, + targets: targets.length, + permissionLevel, + }); + + res.json({ + success: true, + permissionLevel, + expiresAt, + hostsShared: sharedCount, + hostsTotal: members.length, + hostResults, + }); + } catch (err) { + databaseLogger.error("Failed to share fleet", err, { + operation: "fleet_share_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to share fleet" }); + } + }, +); + +// Minimal promisified SFTP primitives for whole-file push/pull. Deliberately +// not reusing transfer-engine.ts - that file's resumable/segmented/tar +// machinery is built for the interactive file manager's large-transfer UX +// and is overbuilt for a fleet action operating on one buffered file per host. +function getSftp(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) reject(err); + else resolve(sftp); + }); + }); +} + +function sftpWriteFile( + sftp: SFTPWrapper, + remotePath: string, + data: Buffer, +): Promise { + return new Promise((resolve, reject) => { + const stream = sftp.createWriteStream(remotePath); + stream.on("error", reject); + stream.on("close", resolve); + stream.end(data); + }); +} + +function sftpReadFile(sftp: SFTPWrapper, remotePath: string): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const stream = sftp.createReadStream(remotePath); + stream.on("data", (chunk: Buffer) => chunks.push(chunk)); + stream.on("error", reject); + stream.on("close", () => resolve(Buffer.concat(chunks))); + }); +} + +interface FleetHostResult { + hostId: number; + hostName: string; + success: boolean; + output?: string; + error?: string; +} + +/** + * Resolves a fleet's effective members, re-checks the caller's per-host + * access at `level` (fleet membership alone is not treated as authorization - + * a fleet-sharee can only act on member hosts they individually have access + * to), and runs `fn` against each authorized host concurrently. One host's + * rejection never aborts the others. + */ +async function runAcrossFleet( + userId: string, + fleetId: number, + level: HostAction, + fn: (host: { + id: number; + name: string; + }) => Promise>, +): Promise<{ results: FleetHostResult[]; fleetFound: boolean }> { + const fleetRepository = createCurrentFleetRepository(); + const fleet = await fleetRepository.findById(userId, fleetId); + if (!fleet) { + return { results: [], fleetFound: false }; + } + + const members = await fleetRepository.listEffectiveMembers(userId, fleetId); + + const settled = await Promise.allSettled( + members.map(async (host) => { + const access = await permissionManager.canAccessHost( + userId, + host.id, + level, + ); + if (!access.hasAccess) { + return { + hostId: host.id, + hostName: host.name, + success: false, + error: `Access denied (requires '${level}' level)`, + } satisfies FleetHostResult; + } + + try { + const outcome = await fn({ id: host.id, name: host.name }); + return { + hostId: host.id, + hostName: host.name, + ...outcome, + } satisfies FleetHostResult; + } catch (err) { + return { + hostId: host.id, + hostName: host.name, + success: false, + error: getErrorMessage(err), + } satisfies FleetHostResult; + } + }), + ); + + const results = settled.map((r) => + r.status === "fulfilled" + ? r.value + : ({ + hostId: -1, + hostName: "unknown", + success: false, + error: r.reason instanceof Error ? r.reason.message : "Unknown error", + } satisfies FleetHostResult), + ); + + return { results, fleetFound: true }; +} + +/** + * @openapi + * /fleets/{id}/execute: + * post: + * summary: Run a command across every host in a fleet + * description: Fans out concurrently to every effective member host the caller has edit-level access to. $HOST/$USER/$PORT/$NAME/$INPUT_n substitution is applied per host, same grammar as snippet execution. One host failing does not stop the others. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * command: + * type: string + * inputValues: + * type: object + * responses: + * 200: + * description: Per-host execution results. + * 404: + * description: Fleet not found. + */ +router.post( + "/:id/execute", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + const { command, inputValues } = req.body ?? {}; + + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + if (!isNonEmptyString(command)) { + return res.status(400).json({ error: "Command is required" }); + } + + try { + const { results, fleetFound } = await runAcrossFleet( + userId, + fleetId, + "edit", + async (host) => { + const fullHost = await resolveHostById(host.id, userId); + if (!fullHost) { + return { success: false, error: "Host not found" }; + } + + const resolvedCommand = resolveSnippetCommand( + command, + { + ip: fullHost.ip, + username: fullHost.username, + port: fullHost.port, + name: fullHost.name, + }, + inputValues && typeof inputValues === "object" ? inputValues : {}, + ); + + const { stdout, stderr, code } = await withConnection( + getFleetPoolKey(fullHost), + createFleetSshFactory(fullHost), + (client) => execCommand(client, resolvedCommand, 60000), + ); + + return { + success: code === 0, + output: stdout, + error: + code !== 0 ? stderr || `Exited with code ${code}` : undefined, + }; + }, + ); + + if (!fleetFound) { + return res.status(404).json({ error: "Fleet not found" }); + } + + authLogger.success(`Fleet command executed on fleet ${fleetId}`, { + operation: "fleet_execute_success", + userId, + fleetId, + hostCount: results.length, + }); + + res.json({ results }); + } catch (err) { + databaseLogger.error("Failed to execute fleet command", err, { + operation: "fleet_execute_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to execute fleet command" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/transfer/push: + * post: + * summary: Push an uploaded file to the same remote path on every host in a fleet + * description: Fans out concurrently to every effective member host the caller has edit-level access to. Single file only (v1) - the file is buffered once server-side and written to each host via SFTP. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * file: + * type: string + * format: binary + * remotePath: + * type: string + * responses: + * 200: + * description: Per-host push results. + * 404: + * description: Fleet not found. + */ +router.post( + "/:id/transfer/push", + authenticateJWT, + requireDataAccess, + upload.single("file"), + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + const remotePath = req.body?.remotePath; + const file = (req as Request & { file?: Express.Multer.File }).file; + + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + if (!isNonEmptyString(remotePath)) { + return res.status(400).json({ error: "remotePath is required" }); + } + if (!file) { + return res.status(400).json({ error: "A file is required" }); + } + + try { + const { results, fleetFound } = await runAcrossFleet( + userId, + fleetId, + "edit", + async (host) => { + const fullHost = await resolveHostById(host.id, userId); + if (!fullHost) { + return { success: false, error: "Host not found" }; + } + + await withConnection( + getFleetPoolKey(fullHost), + createFleetSshFactory(fullHost), + async (client) => { + const sftp = await getSftp(client); + await sftpWriteFile(sftp, remotePath, file.buffer); + }, + ); + + return { success: true, output: `Written to ${remotePath}` }; + }, + ); + + if (!fleetFound) { + return res.status(404).json({ error: "Fleet not found" }); + } + + authLogger.success(`Fleet file push executed on fleet ${fleetId}`, { + operation: "fleet_transfer_push_success", + userId, + fleetId, + hostCount: results.length, + }); + + res.json({ results }); + } catch (err) { + databaseLogger.error("Failed to push file across fleet", err, { + operation: "fleet_transfer_push_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to push file across fleet" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/transfer/pull: + * post: + * summary: Pull the same remote path from every host in a fleet + * description: Fans out concurrently to every effective member host the caller has edit-level access to, reads remotePath via SFTP from each, and returns a single zip archive with one entry per successful host (/). Single file only (v1). + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * remotePath: + * type: string + * responses: + * 200: + * description: Zip archive containing the per-host pulled files, plus an X-Fleet-Transfer-Results header with the per-host JSON result summary. + * 404: + * description: Fleet not found. + */ +router.post( + "/:id/transfer/pull", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + const { remotePath } = req.body ?? {}; + + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + if (!isNonEmptyString(remotePath)) { + return res.status(400).json({ error: "remotePath is required" }); + } + + try { + const zip = new JSZip(); + const fileName = remotePath.split("/").filter(Boolean).pop() || "file"; + + const { results, fleetFound } = await runAcrossFleet( + userId, + fleetId, + "edit", + async (host) => { + const fullHost = await resolveHostById(host.id, userId); + if (!fullHost) { + return { success: false, error: "Host not found" }; + } + + const data = await withConnection( + getFleetPoolKey(fullHost), + createFleetSshFactory(fullHost), + async (client) => { + const sftp = await getSftp(client); + return sftpReadFile(sftp, remotePath); + }, + ); + + zip.file(`${host.name}/${fileName}`, data); + return { success: true, output: `Pulled ${data.length} bytes` }; + }, + ); + + if (!fleetFound) { + return res.status(404).json({ error: "Fleet not found" }); + } + + authLogger.success(`Fleet file pull executed on fleet ${fleetId}`, { + operation: "fleet_transfer_pull_success", + userId, + fleetId, + hostCount: results.length, + }); + + const archive = await zip.generateAsync({ type: "nodebuffer" }); + res.setHeader("Content-Type", "application/zip"); + res.setHeader( + "Content-Disposition", + `attachment; filename="fleet-${fleetId}-${fileName}.zip"`, + ); + res.setHeader( + "X-Fleet-Transfer-Results", + Buffer.from(JSON.stringify(results)).toString("base64"), + ); + res.send(archive); + } catch (err) { + databaseLogger.error("Failed to pull file across fleet", err, { + operation: "fleet_transfer_pull_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to pull file across fleet" }); + } + }, +); + +// Kernel/arch/hostname/uptime are cheap, always-available facts not covered by +// detectPlatform's tooling probe. One combined command keeps this to a single +// round trip per host, same "key=value per line" shape as PLATFORM_PROBE_COMMAND. +const INVENTORY_PROBE_COMMAND = [ + "echo kernel=$(uname -r)", + "echo arch=$(uname -m)", + "echo hostname=$(hostname)", + "echo uptime_seconds=$(cut -d. -f1 /proc/uptime 2>/dev/null || echo '')", +].join("; "); + +export function parseInventoryProbe(output: string): { + kernel: string | null; + architecture: string | null; + hostname: string | null; + uptimeSeconds: number | null; +} { + const map = new Map(); + for (const line of output.split("\n")) { + const idx = line.indexOf("="); + if (idx === -1) continue; + map.set(line.slice(0, idx).trim(), line.slice(idx + 1).trim()); + } + + const uptimeRaw = map.get("uptime_seconds"); + const uptimeSeconds = + uptimeRaw && /^\d+$/.test(uptimeRaw) ? parseInt(uptimeRaw, 10) : null; + + return { + kernel: map.get("kernel") || null, + architecture: map.get("arch") || null, + hostname: map.get("hostname") || null, + uptimeSeconds, + }; +} + +/** + * @openapi + * /fleets/{id}/inventory: + * get: + * summary: Read the last-known inventory snapshot for a fleet's members + * description: No live connection - reads back whatever the most recent POST refresh stored. Latest-only per host, no history. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Stored inventory rows for current members. + * 404: + * description: Fleet not found. + */ +router.get( + "/:id/inventory", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + + try { + const fleetRepository = createCurrentFleetRepository(); + const fleet = await fleetRepository.findById(userId, fleetId); + if (!fleet) { + return res.status(404).json({ error: "Fleet not found" }); + } + + const members = await fleetRepository.listEffectiveMembers( + userId, + fleetId, + ); + const inventory = + await createCurrentFleetInventoryRepository().listForHosts( + userId, + members.map((m) => m.id), + ); + + const byHostId = new Map(inventory.map((row) => [row.hostId, row])); + res.json( + members.map((host) => ({ + hostId: host.id, + hostName: host.name, + inventory: byHostId.get(host.id) ?? null, + })), + ); + } catch (err) { + databaseLogger.error("Failed to read fleet inventory", err, { + operation: "fleet_inventory_read_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to read fleet inventory" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/inventory: + * post: + * summary: Refresh the inventory snapshot for every host in a fleet + * description: Connects to every effective member host the caller has view-level access to, collects OS/kernel/arch/hostname/uptime, and overwrites the stored latest-only snapshot per host. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Per-host refresh results. + * 404: + * description: Fleet not found. + */ +router.post( + "/:id/inventory", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + + try { + const inventoryRepository = createCurrentFleetInventoryRepository(); + + const { results, fleetFound } = await runAcrossFleet( + userId, + fleetId, + "view", + async (host) => { + const fullHost = await resolveHostById(host.id, userId); + if (!fullHost) { + return { success: false, error: "Host not found" }; + } + + const record = await withConnection( + getFleetPoolKey(fullHost), + createFleetSshFactory(fullHost), + async (client) => { + const platform = await detectPlatform(client); + const { stdout } = await execCommand( + client, + INVENTORY_PROBE_COMMAND, + 15000, + ); + const facts = parseInventoryProbe(stdout); + + return inventoryRepository.upsert(userId, host.id, { + osPrettyName: platform.osPrettyName, + kernel: facts.kernel, + architecture: facts.architecture, + hostname: facts.hostname, + uptimeSeconds: facts.uptimeSeconds, + ip: fullHost.ip, + packageManager: platform.pkg, + }); + }, + ); + + return { + success: true, + output: JSON.stringify(record), + }; + }, + ); + + if (!fleetFound) { + return res.status(404).json({ error: "Fleet not found" }); + } + + res.json({ results }); + } catch (err) { + databaseLogger.error("Failed to refresh fleet inventory", err, { + operation: "fleet_inventory_refresh_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to refresh fleet inventory" }); + } + }, +); + +/** + * @openapi + * /fleets/{id}/packages: + * post: + * summary: Run a package action across every host in a fleet + * description: Auto-detects each host's package manager (apt/dnf/yum/pacman) and runs install/remove/upgrade-all, elevating with the host's stored sudo password. Requires manage-level access per host. + * tags: + * - Fleets + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * action: + * type: string + * enum: [install, remove, upgrade-all] + * package: + * type: string + * responses: + * 200: + * description: Per-host package action results. + * 404: + * description: Fleet not found. + */ +router.post( + "/:id/packages", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const fleetId = parseFleetId(req.params.id); + const { action, package: packageName } = req.body ?? {}; + + if (fleetId === null) { + return res.status(400).json({ error: "Invalid fleet ID" }); + } + if ( + action !== "install" && + action !== "remove" && + action !== "upgrade-all" + ) { + return res.status(400).json({ error: "Invalid action" }); + } + if (action !== "upgrade-all" && !isValidPackageName(packageName)) { + return res.status(400).json({ error: "Invalid package name" }); + } + + try { + const { results, fleetFound } = await runAcrossFleet( + userId, + fleetId, + "manage", + async (host) => { + const fullHost = await resolveHostById(host.id, userId); + if (!fullHost) { + return { success: false, error: "Host not found" }; + } + + return withConnection( + getFleetPoolKey(fullHost), + createFleetSshFactory(fullHost), + async (client) => { + const platform = await detectPlatform(client); + // "remove" has no PackageAction counterpart in buildPackageActionCommand + // (install/upgrade-all only) - build it here per distro instead. + const cmd = + action === "remove" + ? buildRemoveCommand(platform.pkg, packageName) + : buildPackageActionCommand( + platform.pkg, + action, + packageName, + ); + + if (!cmd) { + return { + success: false, + error: platform.pkg + ? `Unsupported package action '${action}' for ${platform.pkg}` + : "No supported package manager detected on this host", + }; + } + + try { + const result = await execElevated( + client, + cmd, + fullHost.sudoPassword, + { forceSudo: true, timeoutMs: 600000 }, + ); + return { + success: result.code === 0, + output: (result.stdout || result.stderr).slice(-8000), + }; + } catch (elevationError) { + if (elevationError instanceof ElevationError) { + return { success: false, error: elevationError.message }; + } + throw elevationError; + } + }, + ); + }, + ); + + if (!fleetFound) { + return res.status(404).json({ error: "Fleet not found" }); + } + + res.json({ results }); + } catch (err) { + databaseLogger.error("Failed to run fleet package action", err, { + operation: "fleet_packages_failed", + userId, + fleetId, + }); + res.status(500).json({ error: "Failed to run fleet package action" }); + } + }, +); + +export function buildRemoveCommand( + pkg: "apt" | "dnf" | "yum" | "pacman" | null, + name: string, +): string | null { + switch (pkg) { + case "apt": + return `DEBIAN_FRONTEND=noninteractive apt-get -y remove ${name}`; + case "dnf": + return `dnf -y remove ${name}`; + case "yum": + return `yum -y remove ${name}`; + case "pacman": + return `pacman -R --noconfirm ${name}`; + default: + return null; + } +} + +export default router; diff --git a/src/backend/database/routes/homepage-favicon-routes.ts b/src/backend/database/routes/homepage-favicon-routes.ts index 8f048ee..77d3d33 100644 --- a/src/backend/database/routes/homepage-favicon-routes.ts +++ b/src/backend/database/routes/homepage-favicon-routes.ts @@ -1,6 +1,5 @@ -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; -import express from "express"; import https from "https"; import http from "http"; diff --git a/src/backend/database/routes/homepage-items-routes.ts b/src/backend/database/routes/homepage-items-routes.ts index 5ea4979..86c5370 100644 --- a/src/backend/database/routes/homepage-items-routes.ts +++ b/src/backend/database/routes/homepage-items-routes.ts @@ -1,8 +1,10 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; -import { createCurrentHomepageItemRepository } from "../repositories/factory.js"; -import express from "express"; +import { + createCurrentHomepageItemRepository, + createCurrentSyncTombstoneRepository, +} from "../repositories/factory.js"; export const homepageItemsRouter = express.Router(); @@ -184,7 +186,14 @@ homepageItemsRouter.delete("/:id", async (req: Request, res: Response) => { return res.status(404).json({ error: "Not found" }); } - await itemRepository.deleteForUser(userId, id); + const deleted = await itemRepository.deleteForUser(userId, id); + if (deleted?.syncId) { + await createCurrentSyncTombstoneRepository().record( + userId, + "homepageItems", + deleted.syncId, + ); + } res.json({ message: "Homepage item deleted" }); } catch (err) { homepageLogger.error("Failed to delete homepage item", err); diff --git a/src/backend/database/routes/homepage-layout-routes.ts b/src/backend/database/routes/homepage-layout-routes.ts index a02ba5a..5f7ced5 100644 --- a/src/backend/database/routes/homepage-layout-routes.ts +++ b/src/backend/database/routes/homepage-layout-routes.ts @@ -1,7 +1,6 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; -import express from "express"; import { createCurrentHomepageLayoutRepository } from "../repositories/factory.js"; export const homepageLayoutRouter = express.Router(); diff --git a/src/backend/database/routes/homepage-ping-routes.ts b/src/backend/database/routes/homepage-ping-routes.ts index 7b2995c..24bdee9 100644 --- a/src/backend/database/routes/homepage-ping-routes.ts +++ b/src/backend/database/routes/homepage-ping-routes.ts @@ -1,8 +1,6 @@ -import type { Request, Response } from "express"; -import express from "express"; -import https from "https"; -import http from "http"; +import express, { type Request, type Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; +import { safeOutboundFetch } from "../../utils/safe-outbound-fetch.js"; export const homepagePingRouter = express.Router(); @@ -17,54 +15,37 @@ const pingCache = new Map(); const CACHE_SIZE = 200; const FETCH_TIMEOUT_MS = 5000; -function pingUrl( +async function requestStatus( + url: string, + method: "HEAD" | "GET", +): Promise { + const res = await safeOutboundFetch(url, { + method, + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }); + // Discard the body without buffering it. + await res.body?.cancel().catch(() => {}); + return res.status ?? null; +} + +async function pingUrl( url: string, ): Promise<{ ok: boolean; statusCode: number | null; latencyMs: number }> { - return new Promise((resolve) => { - const start = performance.now(); - const mod = url.startsWith("https") ? https : http; - - const done = (ok: boolean, statusCode: number | null) => { - resolve({ - ok, - statusCode, - latencyMs: Math.round(performance.now() - start), - }); + const start = performance.now(); + const elapsed = () => Math.round(performance.now() - start); + try { + let code = await requestStatus(url, "HEAD"); + if (code === 405) { + code = await requestStatus(url, "GET"); + } + return { + ok: code !== null && code < 400, + statusCode: code, + latencyMs: elapsed(), }; - - const tryGet = () => { - const req = mod.get(url, { timeout: FETCH_TIMEOUT_MS }, (res) => { - res.resume(); - const code = res.statusCode ?? null; - done(code !== null && code < 400, code); - }); - req.on("error", () => done(false, null)); - req.on("timeout", () => { - req.destroy(); - done(false, null); - }); - }; - - const req = mod.request( - url, - { method: "HEAD", timeout: FETCH_TIMEOUT_MS }, - (res) => { - res.resume(); - const code = res.statusCode ?? null; - if (code === 405) { - tryGet(); - } else { - done(code !== null && code < 400, code); - } - }, - ); - req.on("error", () => done(false, null)); - req.on("timeout", () => { - req.destroy(); - done(false, null); - }); - req.end(); - }); + } catch { + return { ok: false, statusCode: null, latencyMs: elapsed() }; + } } /** diff --git a/src/backend/database/routes/homepage-proxy-routes.ts b/src/backend/database/routes/homepage-proxy-routes.ts index eaf23a1..930cc2b 100644 --- a/src/backend/database/routes/homepage-proxy-routes.ts +++ b/src/backend/database/routes/homepage-proxy-routes.ts @@ -1,10 +1,11 @@ -import type { Request, Response } from "express"; -import express from "express"; +import { getErrorMessage } from "../../utils/error-message.js"; +import express, { type Request, type Response } from "express"; import https from "https"; import http from "http"; import { lookup } from "dns/promises"; -import { BlockList, isIP } from "net"; +import { isIP } from "net"; import { homepageLogger } from "../../utils/logger.js"; +import { isBlockedAddress } from "../../utils/safe-outbound-fetch.js"; export const homepageProxyRouter = express.Router(); @@ -17,40 +18,6 @@ const proxyCache = new Map(); const CACHE_SIZE = 50; const FETCH_TIMEOUT_MS = 8000; -const blockedAddresses = new BlockList(); -for (const [network, prefix] of [ - ["0.0.0.0", 8], - ["10.0.0.0", 8], - ["100.64.0.0", 10], - ["127.0.0.0", 8], - ["169.254.0.0", 16], - ["172.16.0.0", 12], - ["192.168.0.0", 16], - ["198.18.0.0", 15], - ["224.0.0.0", 4], - ["240.0.0.0", 4], -] as const) { - blockedAddresses.addSubnet(network, prefix, "ipv4"); -} -for (const [network, prefix] of [ - ["::", 128], - ["::1", 128], - ["::ffff:0:0", 96], - ["fc00::", 7], - ["fe80::", 10], - ["ff00::", 8], -] as const) { - blockedAddresses.addSubnet(network, prefix, "ipv6"); -} - -function isBlockedAddress(address: string): boolean { - const family = isIP(address); - return ( - family === 0 || - blockedAddresses.check(address, family === 4 ? "ipv4" : "ipv6") - ); -} - async function resolvePublicUrl(rawUrl: string): Promise<{ url: URL; address: string; @@ -165,7 +132,7 @@ homepageProxyRouter.get("/", async (req: Request, res: Response) => { proxyCache.set(targetUrl, { data, expires: Date.now() + ttl }); res.json(data); } catch (err) { - const msg = err instanceof Error ? err.message : "Unknown error"; + const msg = getErrorMessage(err); homepageLogger.warn("Proxy fetch failed", { targetUrl, msg }); if (msg.includes("not valid JSON")) { return res.status(400).json({ error: "Response is not valid JSON" }); diff --git a/src/backend/database/routes/homepage-rss-routes.ts b/src/backend/database/routes/homepage-rss-routes.ts index 7312243..e42cd51 100644 --- a/src/backend/database/routes/homepage-rss-routes.ts +++ b/src/backend/database/routes/homepage-rss-routes.ts @@ -1,8 +1,6 @@ -import type { Request, Response } from "express"; -import express from "express"; -import https from "https"; -import http from "http"; +import express, { type Request, type Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; +import { safeOutboundFetch } from "../../utils/safe-outbound-fetch.js"; export const homepageRssRouter = express.Router(); @@ -19,19 +17,11 @@ interface RssItem { } function fetchXml(url: string): Promise { - return new Promise((resolve, reject) => { - const mod = url.startsWith("https") ? https : http; - const req = mod.get(url, { timeout: FETCH_TIMEOUT_MS }, (res) => { - const chunks: Buffer[] = []; - res.on("data", (chunk: Buffer) => chunks.push(chunk)); - res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8"))); - res.on("error", reject); - }); - req.on("error", reject); - req.on("timeout", () => { - req.destroy(); - reject(new Error("RSS fetch timeout")); - }); + return safeOutboundFetch(url, { + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }).then(async (res) => { + if (!res.ok) throw new Error(`RSS fetch failed: ${res.status}`); + return res.text(); }); } diff --git a/src/backend/database/routes/host-autostart-routes.ts b/src/backend/database/routes/host-autostart-routes.ts index 42aa427..7c72d8d 100644 --- a/src/backend/database/routes/host-autostart-routes.ts +++ b/src/backend/database/routes/host-autostart-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { Request, RequestHandler, Response, Router } from "express"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { DataCrypto } from "../../utils/data-crypto.js"; @@ -148,7 +149,7 @@ export function registerHostAutostartRoutes( } catch (error) { sshLogger.warn("Failed to update tunnel connections", { operation: "tunnel_connections_update_failed", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } diff --git a/src/backend/database/routes/host-bulk-routes.ts b/src/backend/database/routes/host-bulk-routes.ts index 2838a4b..a575dae 100644 --- a/src/backend/database/routes/host-bulk-routes.ts +++ b/src/backend/database/routes/host-bulk-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, RequestHandler, Response, Router } from "express"; import { sshLogger } from "../../utils/logger.js"; @@ -6,6 +7,7 @@ import { createCurrentHostRepository, createCurrentHostResolutionRepository, } from "../repositories/factory.js"; +import { validateParentHostId } from "./host-parent-validation.js"; import { isNonEmptyString, isValidPort, @@ -154,6 +156,13 @@ export function registerHostBulkRoutes( * type: number * updates: * type: object + * description: Partial fields to apply. Setting folder clears parentHostId and vice versa, since a host is either in a folder or nested under a parent host. + * properties: + * folder: + * type: string + * parentHostId: + * type: integer + * nullable: true * responses: * 200: * description: Bulk update completed. @@ -215,8 +224,39 @@ export function registerHostBulkRoutes( const simpleUpdates: Record = {}; if (typeof updates.pin === "boolean") simpleUpdates.pin = updates.pin; - if (typeof updates.folder === "string") + if (typeof updates.folder === "string") { simpleUpdates.folder = updates.folder || null; + // Folder placement and parent-host placement are mutually + // exclusive -- assigning a folder (including moving to root, an + // empty folder) clears any parent host, matching the single-host + // update route's behavior. + simpleUpdates.parentHostId = null; + } + if (updates.parentHostId !== undefined) { + if (updates.parentHostId === null) { + simpleUpdates.parentHostId = null; + } else { + const numericParentHostId = Number(updates.parentHostId); + if (!Number.isInteger(numericParentHostId)) { + return res.status(400).json({ error: "Invalid parent host" }); + } + // A bulk move can only ever target one parent host at a time + // (the caller drags a selection onto one drop target), so every + // id in the batch is checked against the same candidate parent. + for (const id of ownedIds) { + const parentError = await validateParentHostId( + userId, + id, + numericParentHostId, + ); + if (parentError) { + return res.status(400).json({ error: parentError }); + } + } + simpleUpdates.parentHostId = numericParentHostId; + simpleUpdates.folder = null; + } + } if (typeof updates.enableTerminal === "boolean") simpleUpdates.enableTerminal = updates.enableTerminal; if (typeof updates.enableTunnel === "boolean") @@ -227,6 +267,8 @@ export function registerHostBulkRoutes( simpleUpdates.enableDocker = updates.enableDocker; if (typeof updates.enableTmuxMonitor === "boolean") simpleUpdates.enableTmuxMonitor = updates.enableTmuxMonitor; + if (typeof updates.enableTerminalToolbar === "boolean") + simpleUpdates.enableTerminalToolbar = updates.enableTerminalToolbar; // Disabling Proxmox is a plain flag flip; enabling is handled per-host // below so each host can default to its own stored credential. if (updates.enableProxmox === false) @@ -299,6 +341,84 @@ export function registerHostBulkRoutes( }, ); + /** + * @openapi + * /host/reorder: + * put: + * summary: Reorder hosts + * description: Sets a manual sortOrder for multiple hosts within the same folder, used by drag-to-reorder in the sidebar's manual sort mode. + * tags: + * - SSH + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * positions: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * sortOrder: + * type: integer + * responses: + * 200: + * description: Hosts reordered successfully. + * 400: + * description: Invalid positions array. + * 500: + * description: Failed to reorder hosts. + */ + router.put( + "/reorder", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { positions } = req.body as { + positions?: { id?: unknown; sortOrder?: unknown }[]; + }; + + if (!Array.isArray(positions)) { + return res.status(400).json({ error: "positions array is required" }); + } + + const normalized: { id: number; sortOrder: number }[] = []; + for (const entry of positions) { + if ( + typeof entry?.id !== "number" || + !Number.isInteger(entry.id) || + typeof entry.sortOrder !== "number" || + !Number.isFinite(entry.sortOrder) + ) { + return res.status(400).json({ + error: + "Each position requires an integer id and a numeric sortOrder", + }); + } + normalized.push({ id: entry.id, sortOrder: entry.sortOrder }); + } + + if (normalized.length === 0) { + return res.status(400).json({ error: "positions array is required" }); + } + + try { + const updated = await createCurrentHostRepository().reorderForUser( + userId, + normalized, + ); + return res.json({ updated }); + } catch (error) { + sshLogger.error("Failed to reorder hosts:", error); + return res.status(500).json({ error: "Failed to reorder hosts" }); + } + }, + ); + router.post( "/bulk-import", authenticateJWT, @@ -391,7 +511,7 @@ export function registerHostBulkRoutes( } } catch (error) { results.errors.push( - `Credential placeholders: ${error instanceof Error ? error.message : "failed to prepare credential aliases"}`, + `Credential placeholders: ${getErrorMessage(error, "failed to prepare credential aliases")}`, ); } @@ -553,6 +673,7 @@ export function registerHostBulkRoutes( enableDocker: hostData.enableDocker || false, enableProxmox: hostData.enableProxmox || false, enableTmuxMonitor: hostData.enableTmuxMonitor || false, + enableTerminalToolbar: hostData.enableTerminalToolbar !== false, showTerminalInSidebar: hostData.showTerminalInSidebar ? 1 : 0, showFileManagerInSidebar: hostData.showFileManagerInSidebar ? 1 : 0, showTunnelInSidebar: hostData.showTunnelInSidebar ? 1 : 0, @@ -665,9 +786,7 @@ export function registerHostBulkRoutes( } } catch (error) { results.failed++; - results.errors.push( - `Host ${i + 1}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); + results.errors.push(`Host ${i + 1}: ${getErrorMessage(error)}`); } } @@ -821,6 +940,7 @@ export function registerHostBulkRoutes( enableDocker: false, enableProxmox: false, enableTmuxMonitor: false, + enableTerminalToolbar: true, showTerminalInSidebar: 0, showFileManagerInSidebar: 0, showTunnelInSidebar: 0, @@ -872,7 +992,7 @@ export function registerHostBulkRoutes( } catch (error) { results.failed++; results.errors.push( - `Host "${parsed[i].name}": ${error instanceof Error ? error.message : "Unknown error"}`, + `Host "${parsed[i].name}": ${getErrorMessage(error)}`, ); } } diff --git a/src/backend/database/routes/host-folder-routes.ts b/src/backend/database/routes/host-folder-routes.ts index dfde149..0c11c47 100644 --- a/src/backend/database/routes/host-folder-routes.ts +++ b/src/backend/database/routes/host-folder-routes.ts @@ -3,6 +3,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import { databaseLogger, sshLogger } from "../../utils/logger.js"; import { createCurrentCommandHistoryRepository, + createCurrentCredentialRepository, createCurrentFileManagerBookmarkRepository, createCurrentHostFolderRepository, createCurrentRecentActivityRepository, @@ -10,6 +11,7 @@ import { createCurrentSshCredentialUsageRepository, createCurrentSessionRecordingRepository, createCurrentTransferRecentRepository, + createCurrentSyncTombstoneRepository, } from "../repositories/factory.js"; import { isNonEmptyString } from "./host-normalizers.js"; @@ -138,7 +140,7 @@ export function registerHostFolderRoutes( * /host/folders/metadata: * put: * summary: Update folder metadata - * description: Updates the metadata (color, icon) of a folder. + * description: Updates the metadata (color, icon, assigned credential) of a folder. * tags: * - SSH * requestBody: @@ -154,6 +156,9 @@ export function registerHostFolderRoutes( * type: string * icon: * type: string + * credentialId: + * type: integer + * nullable: true * responses: * 200: * description: Folder metadata updated successfully. @@ -167,19 +172,46 @@ export function registerHostFolderRoutes( authenticateJWT, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; - const { name, color, icon } = req.body; + const { name, color, icon, credentialId } = req.body; if (!isNonEmptyString(userId) || !name) { return res.status(400).json({ error: "Folder name is required" }); } + const normalizedCredentialId = + credentialId === undefined + ? undefined + : credentialId === null || credentialId === "" + ? null + : Number(credentialId); + + if ( + normalizedCredentialId !== undefined && + normalizedCredentialId !== null && + !Number.isInteger(normalizedCredentialId) + ) { + return res.status(400).json({ error: "Invalid credential ID" }); + } + try { + if (normalizedCredentialId) { + const credential = + await createCurrentCredentialRepository().findByIdForUser( + userId, + normalizedCredentialId, + ); + if (!credential) { + return res.status(404).json({ error: "Credential not found" }); + } + } + const { folder, created } = await createCurrentHostFolderRepository().upsertMetadata( userId, name, color, icon, + normalizedCredentialId, ); if (!created) { @@ -208,6 +240,87 @@ export function registerHostFolderRoutes( }, ); + /** + * @openapi + * /host/folders/reorder: + * put: + * summary: Reorder folders + * description: Sets a manual sortOrder for multiple sibling folders, used by drag-to-reorder in the sidebar's manual sort mode. Folders with no existing metadata row are created. + * tags: + * - SSH + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * positions: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * sortOrder: + * type: integer + * responses: + * 200: + * description: Folders reordered successfully. + * 400: + * description: Invalid positions array. + * 500: + * description: Failed to reorder folders. + */ + router.put( + "/folders/reorder", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { positions } = req.body as { + positions?: { name?: unknown; sortOrder?: unknown }[]; + }; + + if (!isNonEmptyString(userId) || !Array.isArray(positions)) { + return res.status(400).json({ error: "positions array is required" }); + } + + const normalized: { name: string; sortOrder: number }[] = []; + for (const entry of positions) { + if ( + typeof entry?.name !== "string" || + !entry.name || + typeof entry.sortOrder !== "number" || + !Number.isFinite(entry.sortOrder) + ) { + return res.status(400).json({ + error: "Each position requires a name and a numeric sortOrder", + }); + } + normalized.push({ name: entry.name, sortOrder: entry.sortOrder }); + } + + if (normalized.length === 0) { + return res.status(400).json({ error: "positions array is required" }); + } + + try { + const updated = + await createCurrentHostFolderRepository().reorderFolders( + userId, + normalized, + ); + res.json({ updated }); + } catch (err) { + sshLogger.error("Failed to reorder folders", err, { + operation: "folders_reorder", + userId, + }); + res.status(500).json({ error: "Failed to reorder folders" }); + } + }, + ); + /** * @openapi * /host/folders/{name}/hosts: @@ -287,9 +400,17 @@ export function registerHostFolderRoutes( ); } - await hostFolderRepository.deleteHostsAndFolderRecords( + const { hostSyncIds, folderSyncIds } = + await hostFolderRepository.deleteHostsAndFolderRecords( + userId, + folderName, + ); + const tombstoneRepository = createCurrentSyncTombstoneRepository(); + await tombstoneRepository.recordMany(userId, "hosts", hostSyncIds); + await tombstoneRepository.recordMany( userId, - folderName, + "sshFolders", + folderSyncIds, ); try { diff --git a/src/backend/database/routes/host-network-routes.ts b/src/backend/database/routes/host-network-routes.ts index 6050efe..257ecdf 100644 --- a/src/backend/database/routes/host-network-routes.ts +++ b/src/backend/database/routes/host-network-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, RequestHandler, Response, Router } from "express"; import { sendWakeOnLan, isValidMac } from "../../utils/wake-on-lan.js"; @@ -83,7 +84,7 @@ export function registerHostNetworkRoutes( }); res.status(500).json({ success: false, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } }, @@ -132,10 +133,7 @@ export function registerHostNetworkRoutes( hostId, }); res.status(500).json({ - error: - error instanceof Error - ? error.message - : "Failed to send WoL packet", + error: getErrorMessage(error, "Failed to send WoL packet"), }); } }, diff --git a/src/backend/database/routes/host-normalizers.ts b/src/backend/database/routes/host-normalizers.ts index fd406e5..510198a 100644 --- a/src/backend/database/routes/host-normalizers.ts +++ b/src/backend/database/routes/host-normalizers.ts @@ -1,3 +1,5 @@ +import type { AuthOverrideProtocol } from "../../../types/auth-protocols.js"; + export function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } @@ -6,6 +8,56 @@ export function isValidPort(port: unknown): port is number { return typeof port === "number" && port > 0 && port <= 65535; } +export function isOptionalBoolean( + value: unknown, +): value is boolean | undefined { + return value === undefined || typeof value === "boolean"; +} + +export const OWNER_PRIVATE_AUTH_FIELDS = { + ssh: [ + "authType", + "authMethod", + "credentialId", + "vaultProfileId", + "overrideCredentialUsername", + "shareSshAuth", + "password", + "key", + "keyPassword", + "keyType", + "sudoPassword", + ], + rdp: [ + "rdpAuthType", + "rdpCredentialId", + "rdpUser", + "rdpPassword", + "rdpDomain", + ], + vnc: ["vncAuthType", "vncCredentialId", "vncUser", "vncPassword"], + telnet: [ + "telnetAuthType", + "telnetCredentialId", + "telnetUser", + "telnetPassword", + ], +} as const satisfies Record; + +export const OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS = [ + "sudoPassword", + "agentSocketPath", +] as const; + +export function containsOwnerPrivateAuthUpdate( + hostData: Record, + protocol: AuthOverrideProtocol, +): boolean { + return OWNER_PRIVATE_AUTH_FIELDS[protocol].some((field) => + Object.prototype.hasOwnProperty.call(hostData, field), + ); +} + export const FOLDER_PATH_SEPARATOR = " / "; /** @@ -102,6 +154,7 @@ export type NormalizedImportedHost = Record & { enableDocker?: unknown; enableProxmox?: unknown; enableTmuxMonitor?: unknown; + enableTerminalToolbar?: unknown; showTerminalInSidebar?: unknown; showFileManagerInSidebar?: unknown; showTunnelInSidebar?: unknown; @@ -115,6 +168,8 @@ export type NormalizedImportedHost = Record & { statsConfig?: unknown; dockerConfig?: unknown; proxmoxConfig?: unknown; + enableProxmoxStats?: unknown; + proxmoxStatsConfig?: unknown; terminalConfig?: unknown; forceKeyboardInteractive?: unknown; notes?: unknown; @@ -221,13 +276,34 @@ export function stripSensitiveFields( host: Record, ): Record { const result = { ...host }; + const terminalConfigForSudo = + host.terminalConfig && + typeof host.terminalConfig === "object" && + !Array.isArray(host.terminalConfig) + ? (host.terminalConfig as Record) + : undefined; result.hasKey = !!host.key; result.hasKeyPassword = !!host.keyPassword; result.hasPassword = !!host.password; - result.hasSudoPassword = !!host.sudoPassword; + result.hasSudoPassword = + !!host.sudoPassword || !!terminalConfigForSudo?.sudoPassword; + result.hasRdpPassword = !!host.rdpPassword; + result.hasVncPassword = !!host.vncPassword; + result.hasTelnetPassword = !!host.telnetPassword; for (const field of SENSITIVE_FIELDS) { delete result[field]; } + if ( + result.terminalConfig && + typeof result.terminalConfig === "object" && + !Array.isArray(result.terminalConfig) + ) { + const terminalConfig = { + ...(result.terminalConfig as Record), + }; + delete terminalConfig.sudoPassword; + result.terminalConfig = terminalConfig; + } return result; } @@ -248,14 +324,17 @@ const CONNECT_LEVEL_FIELDS = new Set([ "tags", "pin", "authType", + "shareSshAuth", + "authOverrides", "connectionType", - "credentialId", "enableTerminal", "enableTunnel", "enableFileManager", "enableDocker", "enableProxmox", + "enableProxmoxStats", "enableTmuxMonitor", + "enableTerminalToolbar", "showTerminalInSidebar", "showFileManagerInSidebar", "showTunnelInSidebar", @@ -287,6 +366,41 @@ export function sanitizeHostForRecipient( permissionLevel: string | undefined, ): Record { const stripped = stripSensitiveFields(host); + delete stripped.credentialId; + delete stripped.overrideCredentialUsername; + // Sub-host nesting is per-owner tree structure; a recipient generally + // can't see (or share permission on) the parent host row, so a shared + // host always renders at root rather than leaking another host's id. + delete stripped.parentHostId; + if ( + stripped.terminalConfig && + typeof stripped.terminalConfig === "object" && + !Array.isArray(stripped.terminalConfig) + ) { + const terminalConfig = { + ...(stripped.terminalConfig as Record), + }; + delete terminalConfig.agentSocketPath; + stripped.terminalConfig = terminalConfig; + } + const authOverrides = + stripped.authOverrides && + typeof stripped.authOverrides === "object" && + !Array.isArray(stripped.authOverrides) + ? (stripped.authOverrides as Record) + : undefined; + const sshOverride = + authOverrides?.ssh && + typeof authOverrides.ssh === "object" && + !Array.isArray(authOverrides.ssh) + ? (authOverrides.ssh as Record) + : undefined; + if (!sshOverride?.credentialId) { + stripped.hasPassword = false; + stripped.hasKey = false; + stripped.hasKeyPassword = false; + stripped.hasSudoPassword = false; + } if (permissionLevel !== "connect") { return stripped; @@ -313,12 +427,15 @@ export function transformHostResponse( : [] : [], pin: !!host.pin, + shareSshAuth: !!host.shareSshAuth, enableTerminal: !!host.enableTerminal, enableTunnel: !!host.enableTunnel, enableFileManager: host.enableFileManager !== false, enableDocker: !!host.enableDocker, enableProxmox: !!host.enableProxmox, + enableProxmoxStats: !!host.enableProxmoxStats, enableTmuxMonitor: !!host.enableTmuxMonitor, + enableTerminalToolbar: host.enableTerminalToolbar !== false, showTerminalInSidebar: !!host.showTerminalInSidebar, showFileManagerInSidebar: !!host.showFileManagerInSidebar, showTunnelInSidebar: !!host.showTunnelInSidebar, @@ -370,6 +487,9 @@ export function transformHostResponse( proxmoxConfig: host.proxmoxConfig ? JSON.parse(host.proxmoxConfig as string) : undefined, + proxmoxStatsConfig: host.proxmoxStatsConfig + ? JSON.parse(host.proxmoxStatsConfig as string) + : undefined, forceKeyboardInteractive: host.forceKeyboardInteractive === "true", useWarpgate: !!host.useWarpgate, socks5ProxyChain: host.socks5ProxyChain diff --git a/src/backend/database/routes/host-parent-validation.ts b/src/backend/database/routes/host-parent-validation.ts new file mode 100644 index 0000000..d374865 --- /dev/null +++ b/src/backend/database/routes/host-parent-validation.ts @@ -0,0 +1,47 @@ +import { createCurrentHostResolutionRepository } from "../repositories/factory.js"; + +/** + * Validates a proposed parentHostId for a host owned by `userId`. + * + * Rejects a parent that doesn't exist/isn't owned by the same user, a + * self-reference, and any assignment that would create a cycle (the + * candidate parent's own ancestor chain already contains the host being + * assigned). Walks parentHostId in-app rather than via a recursive SQL CTE, + * matching the existing ancestor-walk convention in + * findFolderCredentialId (host-resolution-repository.ts). + * + * `hostId` is null when validating a create (the host doesn't have an id + * yet, so only self-reference/cycle-with-itself is impossible to hit). + */ +export async function validateParentHostId( + userId: string, + hostId: number | null, + parentHostId: number, +): Promise { + if (hostId !== null && parentHostId === hostId) { + return "A host cannot be its own parent"; + } + + const links = + await createCurrentHostResolutionRepository().listOwnHostParentLinks( + userId, + ); + const linksById = new Map(links.map((link) => [link.id, link.parentHostId])); + + if (!linksById.has(parentHostId)) { + return "Parent host not found"; + } + + let current: number | null = parentHostId; + const visited = new Set(); + while (current !== null) { + if (hostId !== null && current === hostId) { + return "That host is a descendant of this host, and cannot be its parent"; + } + if (visited.has(current)) break; + visited.add(current); + current = linksById.get(current) ?? null; + } + + return null; +} diff --git a/src/backend/database/routes/host-sidebar-preferences.ts b/src/backend/database/routes/host-sidebar-preferences.ts new file mode 100644 index 0000000..b7347cf --- /dev/null +++ b/src/backend/database/routes/host-sidebar-preferences.ts @@ -0,0 +1,144 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; +import express, { type Request, type Response } from "express"; +import { databaseLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { + createCurrentHostSidebarPreferenceRepository, + createCurrentUserPreferenceRepository, +} from "../repositories/factory.js"; +import { + defaultHostSidebarPreferences, + sanitizeHostSidebarPreferences, +} from "../../../types/host-sidebar-preferences.js"; + +const router = express.Router(); +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); + +/** + * @openapi + * /host-sidebar/preferences: + * get: + * summary: Get the host sidebar preferences for the current user + * description: Returns the current user's saved sidebar preferences (sort, group, filters, open folders, display settings). On first access, seeds the preferences from the legacy per-column user-preferences fields (showHostTags, hostTrayOnClick, compactHostView, statusColorScheme) so existing settings are not lost. + * tags: + * - Host Sidebar + * responses: + * 200: + * description: The current user's sidebar preferences. + */ +router.get("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const existing = + await createCurrentHostSidebarPreferenceRepository().findByUserId(userId); + + if (existing) { + const preferences = sanitizeHostSidebarPreferences( + JSON.parse(existing.data), + ); + return res.json({ preferences }); + } + + const legacy = + await createCurrentUserPreferenceRepository().findByUserId(userId); + const defaults = defaultHostSidebarPreferences(); + const seeded = sanitizeHostSidebarPreferences({ + ...defaults, + display: { + ...defaults.display, + showTags: legacy?.showHostTags ?? defaults.display.showTags, + trayTrigger: + legacy?.hostTrayOnClick == null + ? defaults.display.trayTrigger + : legacy.hostTrayOnClick + ? "click" + : "hover", + density: + legacy?.compactHostView == null + ? defaults.display.density + : legacy.compactHostView + ? "compact" + : "comfortable", + statusColorScheme: + legacy?.statusColorScheme ?? defaults.display.statusColorScheme, + }, + }); + + await createCurrentHostSidebarPreferenceRepository().upsert( + userId, + JSON.stringify(seeded), + ); + + return res.json({ preferences: seeded }); + } catch (e) { + databaseLogger.error("Failed to get host sidebar preferences", e, { + operation: "get_host_sidebar_preferences", + userId, + }); + return res + .status(500) + .json({ error: "Failed to get host sidebar preferences" }); + } +}); + +/** + * @openapi + * /host-sidebar/preferences: + * put: + * summary: Update the host sidebar preferences for the current user + * description: Persists the current user's sidebar preferences (sort, group, filters, open folders, display settings) as a single JSON document. + * tags: + * - Host Sidebar + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: Preferences updated successfully. + * 400: + * description: Invalid preferences payload. + */ +router.put("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + + if (!req.body || typeof req.body !== "object") { + return res.status(400).json({ error: "Invalid preferences payload" }); + } + + try { + const existing = + await createCurrentHostSidebarPreferenceRepository().findByUserId(userId); + const base = existing + ? sanitizeHostSidebarPreferences(JSON.parse(existing.data)) + : defaultHostSidebarPreferences(); + + const merged = sanitizeHostSidebarPreferences({ + ...base, + ...req.body, + display: { ...base.display, ...(req.body.display ?? {}) }, + sort: { ...base.sort, ...(req.body.sort ?? {}) }, + filters: { ...base.filters, ...(req.body.filters ?? {}) }, + }); + + await createCurrentHostSidebarPreferenceRepository().upsert( + userId, + JSON.stringify(merged), + ); + + return res.json({ success: true, preferences: merged }); + } catch (e) { + databaseLogger.error("Failed to update host sidebar preferences", e, { + operation: "update_host_sidebar_preferences", + userId, + }); + return res + .status(500) + .json({ error: "Failed to update host sidebar preferences" }); + } +}); + +export default router; diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index e4a80dd..7983db2 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -1,6 +1,6 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import axios from "axios"; import multer from "multer"; import { sshLogger, databaseLogger } from "../../utils/logger.js"; @@ -12,8 +12,10 @@ import { pickResolvedPassword, pickResolvedUsername, } from "../../hosts/credential-username.js"; +import { notifyAutomationInternalEvent } from "../../hosts/metrics/automation-bridge.js"; import { createCurrentCommandHistoryRepository, + createCurrentCredentialRepository, createCurrentFileManagerBookmarkRepository, createCurrentOpksshTokenRepository, createCurrentRecentActivityRepository, @@ -25,14 +27,20 @@ import { createCurrentHostResolutionRepository, createCurrentHostRepository, createCurrentUserRepository, + createCurrentSyncTombstoneRepository, } from "../repositories/factory.js"; import { + containsOwnerPrivateAuthUpdate, isNonEmptyString, + isOptionalBoolean, isValidPort, + OWNER_PRIVATE_AUTH_FIELDS, + OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS, sanitizeHostForRecipient, stripSensitiveFields, transformHostResponse, } from "./host-normalizers.js"; +import { validateParentHostId } from "./host-parent-validation.js"; import { registerHostOpksshRoutes } from "./host-opkssh-routes.js"; import { registerHostFolderRoutes } from "./host-folder-routes.js"; import { registerHostFileManagerBookmarkRoutes } from "./host-file-manager-bookmark-routes.js"; @@ -45,7 +53,19 @@ import { applyHostEnrollmentDefaults, requireHostEnrollmentAccessForPath, } from "./host-enrollment-auth.js"; -import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; +import { + logAudit, + getAuditUsername, + getRequestMeta, +} from "../../utils/audit-logger.js"; +import type { + HostResolutionCredentialRecord, + HostResolutionHostRecord, +} from "../repositories/host-resolution-repository.js"; +import { + requiresPersonalHostAuthentication, + resolveRecipientSharedHostAuthentication, +} from "../../utils/shared-host-auth-resolver.js"; const router = express.Router(); @@ -53,11 +73,6 @@ const upload = multer({ storage: multer.memoryStorage() }); const STATS_SERVER_URL = "http://localhost:30005"; -async function getAuditUsername(userId: string): Promise { - const actor = await createCurrentUserRepository().findById(userId); - return actor?.username ?? userId; -} - function notifyStatsHostUpdated( hostId: number, headers: Pick, @@ -152,6 +167,7 @@ router.post( connectionType, name, folder, + parentHostId, tags, ip, port, @@ -160,6 +176,7 @@ router.post( authMethod, authType, useWarpgate, + shareSshAuth, credentialId, vaultProfileId, key, @@ -168,12 +185,15 @@ router.post( sudoPassword, pin, enableTerminal, + enableCommandHistory, enableTunnel, enableFileManager, scpLegacy, enableDocker, enableProxmox, enableTmuxMonitor, + enableTerminalToolbar, + allowSessionSharing, showTerminalInSidebar, showFileManagerInSidebar, showTunnelInSidebar, @@ -186,6 +206,8 @@ router.post( statsConfig, dockerConfig, proxmoxConfig, + enableProxmoxStats, + proxmoxStatsConfig, terminalConfig, forceKeyboardInteractive, domain, @@ -199,6 +221,7 @@ router.post( socks5Username, socks5Password, socks5ProxyChain, + connectionOrigin, portKnockSequence, overrideCredentialUsername, macAddress, @@ -237,7 +260,8 @@ router.post( if ( !isNonEmptyString(userId) || !isNonEmptyString(ip) || - !isValidPort(port) + !isValidPort(port) || + !isOptionalBoolean(shareSshAuth) ) { sshLogger.warn("Invalid SSH data input validation failed", { operation: "host_create", @@ -249,6 +273,23 @@ router.post( return res.status(400).json({ error: "Invalid SSH data" }); } + let validatedParentHostId: number | null = null; + if (parentHostId !== undefined && parentHostId !== null) { + const numericParentHostId = Number(parentHostId); + if (!Number.isInteger(numericParentHostId)) { + return res.status(400).json({ error: "Invalid parent host" }); + } + const parentError = await validateParentHostId( + userId, + null, + numericParentHostId, + ); + if (parentError) { + return res.status(400).json({ error: parentError }); + } + validatedParentHostId = numericParentHostId; + } + const effectiveConnectionType = connectionType || "ssh"; const effectiveAuthType = authType || @@ -262,18 +303,24 @@ router.post( userId: userId, connectionType: effectiveConnectionType, name: effectiveName, - folder: folder || null, + // A host is either placed in a folder or nested under a parent host, + // never both -- setting one clears the other. + folder: validatedParentHostId ? null : folder || null, + parentHostId: validatedParentHostId, tags: Array.isArray(tags) ? tags.join(",") : tags || "", ip, port, username: effectiveUsername, authType: effectiveAuthType, useWarpgate: useWarpgate ? 1 : 0, + shareSshAuth: shareSshAuth === true ? 1 : 0, credentialId: credentialId || null, - vaultProfileId: vaultProfileId || null, + vaultProfileId: + effectiveAuthType === "vault" ? vaultProfileId || null : null, overrideCredentialUsername: overrideCredentialUsername ? 1 : 0, pin: pin ? 1 : 0, enableTerminal: enableTerminal ? 1 : 0, + enableCommandHistory: enableCommandHistory ? 1 : 0, enableTunnel: enableTunnel ? 1 : 0, tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) @@ -287,6 +334,8 @@ router.post( enableDocker: enableDocker ? 1 : 0, enableProxmox: enableProxmox ? 1 : 0, enableTmuxMonitor: enableTmuxMonitor ? 1 : 0, + enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1, + allowSessionSharing: allowSessionSharing === false ? 0 : 1, showTerminalInSidebar: showTerminalInSidebar ? 1 : 0, showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0, showTunnelInSidebar: showTunnelInSidebar ? 1 : 0, @@ -308,6 +357,12 @@ router.post( ? proxmoxConfig : JSON.stringify(proxmoxConfig) : null, + enableProxmoxStats: enableProxmoxStats ? 1 : 0, + proxmoxStatsConfig: proxmoxStatsConfig + ? typeof proxmoxStatsConfig === "string" + ? proxmoxStatsConfig + : JSON.stringify(proxmoxStatsConfig) + : null, terminalConfig: terminalConfig ? typeof terminalConfig === "string" ? terminalConfig @@ -328,6 +383,10 @@ router.post( socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null, + connectionOrigin: + connectionOrigin === "local" || connectionOrigin === "remote" + ? connectionOrigin + : null, macAddress: macAddress || null, wolBroadcastAddress: wolBroadcastAddress || null, portKnockSequence: portKnockSequence @@ -463,7 +522,14 @@ router.post( success: true, }); - res.json(resolvedHost); + notifyAutomationInternalEvent( + "host_added", + userId, + createdHost.id as number, + { name: String(name ?? ip) }, + ); + + res.json(stripSensitiveFields(resolvedHost)); notifyStatsHostUpdated( createdHost.id as number, req.headers, @@ -657,8 +723,7 @@ router.post( } resolvedPassword = pickResolvedPassword(password, cred.password) as - | string - | undefined; + string | undefined; resolvedKey = cred.privateKey as string | undefined; resolvedKeyPassword = cred.keyPassword as string | undefined; resolvedKeyType = cred.keyType as string | undefined; @@ -689,7 +754,9 @@ router.post( enableFileManager: true, enableDocker: false, enableProxmox: false, + enableProxmoxStats: false, enableTmuxMonitor: false, + enableTerminalToolbar: true, showTerminalInSidebar: true, showFileManagerInSidebar: false, showTunnelInSidebar: false, @@ -792,6 +859,7 @@ router.put( connectionType, name, folder, + parentHostId, tags, ip, port, @@ -800,6 +868,7 @@ router.put( authMethod, authType, useWarpgate, + shareSshAuth, credentialId, vaultProfileId, key, @@ -808,12 +877,15 @@ router.put( sudoPassword, pin, enableTerminal, + enableCommandHistory, enableTunnel, enableFileManager, scpLegacy, enableDocker, enableProxmox, enableTmuxMonitor, + enableTerminalToolbar, + allowSessionSharing, showTerminalInSidebar, showFileManagerInSidebar, showTunnelInSidebar, @@ -826,6 +898,8 @@ router.put( statsConfig, dockerConfig, proxmoxConfig, + enableProxmoxStats, + proxmoxStatsConfig, terminalConfig, forceKeyboardInteractive, domain, @@ -839,6 +913,7 @@ router.put( socks5Username, socks5Password, socks5ProxyChain, + connectionOrigin, portKnockSequence, overrideCredentialUsername, macAddress, @@ -878,6 +953,7 @@ router.put( !isNonEmptyString(userId) || !isNonEmptyString(ip) || !isValidPort(port) || + !isOptionalBoolean(shareSshAuth) || !hostId ) { sshLogger.warn("Invalid SSH data input validation failed for update", { @@ -891,6 +967,27 @@ router.put( return res.status(400).json({ error: "Invalid SSH data" }); } + let validatedParentHostId: number | null | undefined = undefined; + if (parentHostId !== undefined) { + if (parentHostId === null) { + validatedParentHostId = null; + } else { + const numericParentHostId = Number(parentHostId); + if (!Number.isInteger(numericParentHostId)) { + return res.status(400).json({ error: "Invalid parent host" }); + } + const parentError = await validateParentHostId( + userId, + Number(hostId), + numericParentHostId, + ); + if (parentError) { + return res.status(400).json({ error: parentError }); + } + validatedParentHostId = numericParentHostId; + } + } + const effectiveAuthType = authType || authMethod; const effectiveUsername = username || rdpUser || vncUser || telnetUser || ""; @@ -899,18 +996,24 @@ router.put( const sshDataObj: Record = { connectionType: connectionType || "ssh", name: effectiveName, - folder, + // A host is either placed in a folder or nested under a parent host, + // never both. When the caller is assigning a parent, clear folder; + // when the caller is assigning a folder, clear parentHostId. + folder: validatedParentHostId ? null : folder, tags: Array.isArray(tags) ? tags.join(",") : tags || "", ip, port, username: effectiveUsername, authType: effectiveAuthType, useWarpgate: useWarpgate ? 1 : 0, + shareSshAuth: shareSshAuth === true ? 1 : 0, credentialId: credentialId || null, - vaultProfileId: vaultProfileId || null, + vaultProfileId: + effectiveAuthType === "vault" ? vaultProfileId || null : null, overrideCredentialUsername: overrideCredentialUsername ? 1 : 0, pin: pin ? 1 : 0, enableTerminal: enableTerminal ? 1 : 0, + enableCommandHistory: enableCommandHistory ? 1 : 0, enableTunnel: enableTunnel ? 1 : 0, tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) @@ -924,6 +1027,8 @@ router.put( enableDocker: enableDocker ? 1 : 0, enableProxmox: enableProxmox ? 1 : 0, enableTmuxMonitor: enableTmuxMonitor ? 1 : 0, + enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1, + allowSessionSharing: allowSessionSharing === false ? 0 : 1, showTerminalInSidebar: showTerminalInSidebar ? 1 : 0, showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0, showTunnelInSidebar: showTunnelInSidebar ? 1 : 0, @@ -945,6 +1050,12 @@ router.put( ? proxmoxConfig : JSON.stringify(proxmoxConfig) : null, + enableProxmoxStats: enableProxmoxStats ? 1 : 0, + proxmoxStatsConfig: proxmoxStatsConfig + ? typeof proxmoxStatsConfig === "string" + ? proxmoxStatsConfig + : JSON.stringify(proxmoxStatsConfig) + : null, terminalConfig: terminalConfig ? typeof terminalConfig === "string" ? terminalConfig @@ -965,6 +1076,10 @@ router.put( socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null, + connectionOrigin: + connectionOrigin === "local" || connectionOrigin === "remote" + ? connectionOrigin + : null, macAddress: macAddress || null, wolBroadcastAddress: wolBroadcastAddress || null, portKnockSequence: portKnockSequence @@ -1067,6 +1182,15 @@ router.put( if (vncPassword) sshDataObj.vncPassword = vncPassword; if (telnetPassword) sshDataObj.telnetPassword = telnetPassword; + if (validatedParentHostId !== undefined) { + sshDataObj.parentHostId = validatedParentHostId; + } else if (folder !== undefined) { + // Caller is assigning a folder (including clearing it back to root) + // without touching parentHostId -- folder placement replaces + // parent-host placement either way. + sshDataObj.parentHostId = null; + } + try { const accessInfo = await permissionManager.canAccessHost( userId, @@ -1100,31 +1224,104 @@ router.put( const ownerId = hostRecord.userId; if (!accessInfo.isOwner) { - // Shared editors work on the owner's real record but may never - // repoint it at credential/vault references (those live in the - // owner's personal vault) or switch the authentication type. + // Shared editors work on the owner's real record, but the owner's SSH + // authentication is private and can only be changed by that owner. + if (containsOwnerPrivateAuthUpdate(hostData, "ssh")) { + return res.status(403).json({ + error: + "Only the host owner can change the host's SSH authentication", + }); + } + + const parseTerminalConfig = ( + value: unknown, + ): Record | null => { + if (!value) return null; + if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) + ) { + return { ...(value as Record) }; + } + if (typeof value === "string") { + const parsed = JSON.parse(value) as unknown; + if ( + typeof parsed === "object" && + parsed !== null && + !Array.isArray(parsed) + ) { + return { ...(parsed as Record) }; + } + } + return null; + }; + + if (hostData.terminalConfig === undefined) { + delete sshDataObj.terminalConfig; + } else { + let incomingTerminalConfig: Record | null; + try { + incomingTerminalConfig = parseTerminalConfig( + hostData.terminalConfig, + ); + } catch { + return res.status(400).json({ error: "Invalid terminal config" }); + } + + if (!incomingTerminalConfig) { + return res.status(400).json({ error: "Invalid terminal config" }); + } + const protectedTerminalConfigField = + OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS.find((field) => + Object.prototype.hasOwnProperty.call( + incomingTerminalConfig, + field, + ), + ); + if (protectedTerminalConfigField) { + return res.status(403).json({ + error: + "Only the host owner can change private SSH authentication settings", + }); + } + + const ownerHost = + await createCurrentHostResolutionRepository().findHostById( + Number(hostId), + ownerId, + ); + const ownerTerminalConfig = parseTerminalConfig( + ownerHost?.terminalConfig, + ); + if (ownerTerminalConfig) { + for (const field of OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS) { + if ( + Object.prototype.hasOwnProperty.call(ownerTerminalConfig, field) + ) { + incomingTerminalConfig[field] = ownerTerminalConfig[field]; + } + } + } + sshDataObj.terminalConfig = JSON.stringify(incomingTerminalConfig); + } + const referenceViolations: Array<[unknown, number | null, string]> = [ - [sshDataObj.credentialId, hostRecord.credentialId, "credential"], [ - sshDataObj.rdpCredentialId, + hostData.rdpCredentialId, hostRecord.rdpCredentialId, "RDP credential", ], [ - sshDataObj.vncCredentialId, + hostData.vncCredentialId, hostRecord.vncCredentialId, "VNC credential", ], [ - sshDataObj.telnetCredentialId, + hostData.telnetCredentialId, hostRecord.telnetCredentialId, "Telnet credential", ], - [ - sshDataObj.vaultProfileId, - hostRecord.vaultProfileId, - "Vault profile", - ], ]; for (const [incoming, current, label] of referenceViolations) { @@ -1135,13 +1332,35 @@ router.put( } } - if ( - sshDataObj.authType !== undefined && - sshDataObj.authType !== hostRecord.authType - ) { - return res.status(403).json({ - error: "Only the host owner can change the authentication type", - }); + for (const field of OWNER_PRIVATE_AUTH_FIELDS.ssh) { + delete sshDataObj[field]; + } + } else if ( + sshDataObj.terminalConfig && + (hostData.terminalConfig as Record | undefined) + ?.sudoPassword === undefined + ) { + // The editor omits sudoPassword entirely when the user hasn't + // touched the field, so preserve whatever is already stored instead + // of letting the wholesale terminalConfig replacement below wipe it. + const existingHost = + await createCurrentHostResolutionRepository().findHostById( + Number(hostId), + ownerId, + ); + const existingTerminalConfig = existingHost?.terminalConfig + ? (JSON.parse(existingHost.terminalConfig as string) as Record< + string, + unknown + >) + : undefined; + if (existingTerminalConfig?.sudoPassword !== undefined) { + const incomingTerminalConfig = JSON.parse( + sshDataObj.terminalConfig as string, + ) as Record; + incomingTerminalConfig.sudoPassword = + existingTerminalConfig.sudoPassword; + sshDataObj.terminalConfig = JSON.stringify(incomingTerminalConfig); } } @@ -1161,10 +1380,7 @@ router.put( sshLogger.warn("Failed to resync shared host secrets after update", { operation: "host_update_resync", hostId: parseInt(hostId), - error: - resyncError instanceof Error - ? resyncError.message - : "Unknown error", + error: getErrorMessage(resyncError), }); } @@ -1206,7 +1422,7 @@ router.put( success: true, }); - res.json(resolvedHost); + res.json(stripSensitiveFields(resolvedHost)); notifyStatsHostUpdated(parseInt(hostId), req.headers, "host_update"); } catch (err) { sshLogger.error("Failed to update SSH host in database", err, { @@ -1286,31 +1502,40 @@ router.get( operation: "host_fetch_own_decrypt_failed", userId, hostId: host.id, - error: - decryptError instanceof Error - ? decryptError.message - : "Unknown error", + error: getErrorMessage(decryptError), }); } } } + // One lookup for every owner rather than one per shared host. const ownerUsernames = new Map(); - const userRepository = createCurrentUserRepository(); - for (const sharedHost of sharedHosts) { - const ownerId = sharedHost.userId as string; - if (!ownerUsernames.has(ownerId)) { - try { - const owner = await userRepository.findById(ownerId); - ownerUsernames.set(ownerId, owner?.username ?? ""); - } catch { - ownerUsernames.set(ownerId, ""); + const ownerIds = Array.from( + new Set(sharedHosts.map((host) => host.userId as string)), + ); + if (ownerIds.length > 0) { + try { + const owners = + await createCurrentUserRepository().listByIds(ownerIds); + for (const owner of owners) { + ownerUsernames.set(owner.id, owner.username ?? ""); } + } catch { + // Falls through to an undefined ownerUsername below. } } const data = [...decryptedOwnHosts, ...sharedHosts]; + // Own hosts all resolve against the caller's own credentials, so they can + // be fetched and decrypted in one batch instead of once per host. + const ownCredentialIds = decryptedOwnHosts + .map((host) => host.credentialId) + .filter((id): id is number => typeof id === "number"); + const credentialsById = await createCurrentHostResolutionRepository() + .listCredentialsByIdsForUser(ownCredentialIds, userId) + .catch(() => new Map()); + const result = await Promise.all( data.map(async (row: Record) => { const baseHost = { @@ -1324,7 +1549,8 @@ router.get( }; const resolved = - (await resolveHostCredentials(baseHost, userId)) || baseHost; + (await resolveHostCredentials(baseHost, userId, credentialsById)) || + baseHost; return resolved; }), ); @@ -1449,9 +1675,14 @@ router.get( sharedExpiresAt: accessInfo.expiresAt || undefined, ownerUsername, }; + const resolvedSharedResult = + (await resolveHostCredentials(sharedResult, userId)) || sharedResult; res.json( - sanitizeHostForRecipient(sharedResult, accessInfo.permissionLevel), + sanitizeHostForRecipient( + resolvedSharedResult, + accessInfo.permissionLevel, + ), ); } catch (err) { sshLogger.error("Failed to fetch SSH host by ID from database", err, { @@ -1482,7 +1713,7 @@ router.get( * name: field * schema: * type: string - * enum: [password, sudoPassword, vncPassword] + * enum: [password, sudoPassword, rdpPassword, vncPassword, telnetPassword, key, keyPassword] * responses: * 200: * description: The requested password value. @@ -1498,15 +1729,26 @@ router.get( const userId = (req as AuthenticatedRequest).userId; const field = (req.query.field as string) || "password"; - if (!["password", "sudoPassword", "vncPassword"].includes(field)) { + if ( + ![ + "password", + "sudoPassword", + "rdpPassword", + "vncPassword", + "telnetPassword", + "key", + "keyPassword", + ].includes(field) + ) { return res.status(400).json({ error: "Invalid field" }); } try { - const host = await createCurrentHostResolutionRepository().findHostById( - hostId, - userId, - ); + const host = + await createCurrentHostResolutionRepository().findHostByIdForUser( + hostId, + userId, + ); if (!host) { return res.status(404).json({ error: "Host not found" }); @@ -1659,7 +1901,9 @@ router.get( scpLegacy: !!resolvedHost.scpLegacy, enableDocker: !!resolvedHost.enableDocker, enableProxmox: !!resolvedHost.enableProxmox, + enableProxmoxStats: !!resolvedHost.enableProxmoxStats, enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor, + enableTerminalToolbar: resolvedHost.enableTerminalToolbar !== false, showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar, showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar, showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar, @@ -1685,6 +1929,9 @@ router.get( proxmoxConfig: resolvedHost.proxmoxConfig ? JSON.parse(resolvedHost.proxmoxConfig as string) : null, + proxmoxStatsConfig: resolvedHost.proxmoxStatsConfig + ? JSON.parse(resolvedHost.proxmoxStatsConfig as string) + : null, terminalConfig: resolvedHost.terminalConfig ? JSON.parse(resolvedHost.terminalConfig as string) : null, @@ -1726,9 +1973,16 @@ router.get( * /host/db/hosts/export: * get: * summary: Export all SSH hosts - * description: Exports all SSH hosts for the current user with decrypted credentials. + * description: Exports all SSH hosts for the current user. By default credentials are decrypted and embedded. With `share=1`, secrets are omitted and credential-authenticated hosts instead reference a scrubbed `credentials` array by alias, suitable for handing off to another user. * tags: * - SSH + * parameters: + * - in: query + * name: share + * required: false + * schema: + * type: string + * description: Set to "1" to export without embedded secrets. * responses: * 200: * description: All exported SSH hosts. @@ -1743,6 +1997,7 @@ router.get( requireDataAccess, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; + const shareMode = req.query.share === "1" || req.query.share === "true"; if (!isNonEmptyString(userId)) { return res.status(400).json({ error: "Invalid userId" }); @@ -1753,10 +2008,12 @@ router.get( await createCurrentHostResolutionRepository().findHostsByUserId(userId); const exportedHosts = []; + const usedCredentialIds = new Set(); for (const host of allHosts) { - const resolvedHost = - (await resolveHostCredentials(host, userId)) || host; + const resolvedHost = shareMode + ? host + : (await resolveHostCredentials(host, userId)) || host; const exportedConnectionType = (resolvedHost.connectionType as string) || "ssh"; @@ -1770,7 +2027,7 @@ router.get( ip: resolvedHost.ip, port: resolvedHost.port, username: resolvedHost.username, - password: resolvedHost.password || null, + password: shareMode ? null : resolvedHost.password || null, folder: resolvedHost.folder, tags: typeof resolvedHost.tags === "string" @@ -1793,8 +2050,8 @@ router.get( : { ...baseExportData, authType: resolvedHost.authType, - key: resolvedHost.key || null, - keyPassword: resolvedHost.keyPassword || null, + key: shareMode ? null : resolvedHost.key || null, + keyPassword: shareMode ? null : resolvedHost.keyPassword || null, keyType: resolvedHost.keyType || null, credentialId: resolvedHost.credentialId || null, overrideCredentialUsername: @@ -1805,13 +2062,17 @@ router.get( enableDocker: !!resolvedHost.enableDocker, enableProxmox: !!resolvedHost.enableProxmox, enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor, + enableTerminalToolbar: + resolvedHost.enableTerminalToolbar !== false, showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar, showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar, showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar, showDockerInSidebar: !!resolvedHost.showDockerInSidebar, showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar, defaultPath: resolvedHost.defaultPath, - sudoPassword: resolvedHost.sudoPassword || null, + sudoPassword: shareMode + ? null + : resolvedHost.sudoPassword || null, tunnelConnections: resolvedHost.tunnelConnections ? JSON.parse(resolvedHost.tunnelConnections as string) : [], @@ -1839,22 +2100,92 @@ router.get( socks5Host: resolvedHost.socks5Host || null, socks5Port: resolvedHost.socks5Port || null, socks5Username: resolvedHost.socks5Username || null, - socks5Password: resolvedHost.socks5Password || null, + socks5Password: shareMode + ? null + : resolvedHost.socks5Password || null, socks5ProxyChain: resolvedHost.socks5ProxyChain ? JSON.parse(resolvedHost.socks5ProxyChain as string) : null, }; + if ( + shareMode && + !isRemoteDesktop && + resolvedHost.authType === "credential" && + resolvedHost.credentialId + ) { + usedCredentialIds.add(resolvedHost.credentialId as number); + } + exportedHosts.push(exportData); } - sshLogger.success("All hosts exported with decrypted credentials", { - operation: "hosts_export_all", + if (!shareMode) { + sshLogger.success("All hosts exported with decrypted credentials", { + operation: "hosts_export_all", + count: exportedHosts.length, + userId, + }); + + return res.json({ hosts: exportedHosts }); + } + + const exportedCredentials: Record[] = []; + if (usedCredentialIds.size > 0) { + const credentialRepository = createCurrentCredentialRepository(); + const ownedCredentials = + await credentialRepository.listDecryptedByUserId(userId); + const credentialById = new Map( + ownedCredentials.map((credential) => [credential.id, credential]), + ); + + for (const host of exportedHosts as Record[]) { + const credentialId = host.credentialId as number | null; + if (!credentialId) continue; + const credential = credentialById.get(credentialId); + if (!credential) continue; + + host.credentialAlias = credential.name; + + if ( + !exportedCredentials.some( + (entry) => entry.alias === credential.name, + ) + ) { + exportedCredentials.push({ + alias: credential.name, + name: credential.name, + description: credential.description || null, + folder: credential.folder || null, + tags: + typeof credential.tags === "string" + ? credential.tags.split(",").filter(Boolean) + : [], + authType: credential.authType, + username: credential.username || null, + keyType: credential.keyType || null, + }); + } + } + } + + for (const host of exportedHosts as Record[]) { + delete host.credentialId; + } + + sshLogger.success("All hosts exported for sharing without secrets", { + operation: "hosts_export_all_share", count: exportedHosts.length, + credentialCount: exportedCredentials.length, userId, }); - res.json({ hosts: exportedHosts }); + res.json({ + version: "1", + exportedAt: new Date().toISOString(), + credentials: exportedCredentials, + hosts: exportedHosts, + }); } catch (err) { sshLogger.error("Failed to export all SSH hosts", err, { operation: "hosts_export_all", @@ -1959,6 +2290,13 @@ router.delete( ); await createCurrentHostRepository().deleteForUser(userId, numericHostId); + if (hostToDelete.syncId) { + await createCurrentSyncTombstoneRepository().record( + userId, + "hosts", + hostToDelete.syncId, + ); + } databaseLogger.success("SSH host deleted", { operation: "host_delete_success", @@ -1979,6 +2317,10 @@ router.delete( success: true, }); + notifyAutomationInternalEvent("host_deleted", userId, numericHostId, { + name: hostToDelete.name ?? hostToDelete.ip, + }); + try { const axios = (await import("axios")).default; await axios.post( @@ -2089,64 +2431,146 @@ registerHostCommandHistoryRoutes(router, authenticateJWT); async function resolveHostCredentials( host: Record, requestingUserId?: string, + /** + * Credentials already fetched for this request, keyed by id. The host list + * preloads them in one query; single-host callers omit it and fall back to + * fetching the one credential they need. + */ + preloadedCredentials?: Map, ): Promise> { try { - if (host.credentialId && (host.userId || host.ownerId)) { - const credentialId = host.credentialId as number; - const ownerId = (host.ownerId || host.userId) as string; + const ownerId = (host.ownerId || host.userId) as string | undefined; + if ( + requestingUserId && + ownerId && + requestingUserId !== ownerId && + typeof host.id === "number" + ) { + const authHost = host as unknown as HostResolutionHostRecord; + const needsPersonalCredential = requiresPersonalHostAuthentication( + authHost, + "ssh", + ); + const baseSshOverrideState = { + required: needsPersonalCredential, + ownerAuthShared: !!host.shareSshAuth, + }; + const recipientHost: Record = { + ...host, + credentialId: null, + password: null, + key: null, + keyPassword: null, + keyType: null, + authOverrides: { + ssh: baseSshOverrideState, + }, + }; - if (requestingUserId && requestingUserId !== ownerId) { - try { - const { SharedHostSecretsManager } = - await import("../../utils/shared-host-secrets-manager.js"); - const sharedCred = - await SharedHostSecretsManager.getInstance().getSecretForUser( - host.id as number, - requestingUserId, - "ssh", - ); + try { + const resolution = await resolveRecipientSharedHostAuthentication( + authHost, + host.id, + requestingUserId, + "ssh", + ); - if (sharedCred) { - const resolvedHost: Record = { - ...host, - password: sharedCred.password, - key: sharedCred.key, - keyPassword: sharedCred.keyPassword, - keyType: sharedCred.keyType, + if (resolution.source === "personal-override") { + const credential = resolution.credential; + return { + ...recipientHost, + authOverrides: { + ssh: { + credentialId: resolution.credentialId, + required: false, + ownerAuthShared: !!host.shareSshAuth, + }, + }, + authType: + credential.key || credential.privateKey + ? "key" + : credential.password + ? "password" + : "none", + username: credential.username || recipientHost.username, + password: credential.password, + key: credential.privateKey || credential.key, + keyPassword: credential.keyPassword, + keyType: credential.keyType, + }; + } + + if (resolution.source === "owner-shared") { + if (resolution.authType === "agent") { + return { + ...recipientHost, + authOverrides: { + ssh: { + required: false, + ownerAuthShared: true, + }, + }, + authType: "agent", }; + } + const sharedAuth = resolution.secret; + if (sharedAuth) { const resolvedUsername = pickResolvedUsername( - host.username, - sharedCred.username, + recipientHost.username, + sharedAuth.username, host.overrideCredentialUsername, ); - if (resolvedUsername !== undefined) { - resolvedHost.username = resolvedUsername; - } - - return resolvedHost; + return { + ...recipientHost, + authOverrides: { + ssh: { + required: false, + ownerAuthShared: true, + }, + }, + authType: sharedAuth.key + ? "key" + : sharedAuth.password + ? "password" + : "none", + username: resolvedUsername, + password: sharedAuth.password, + key: sharedAuth.key, + keyPassword: sharedAuth.keyPassword, + keyType: sharedAuth.keyType, + }; } - } catch (sharedCredError) { - sshLogger.warn( - "Failed to get shared credential, falling back to owner credential", - { - operation: "resolve_shared_credential_fallback", - hostId: host.id as number, - requestingUserId, - error: - sharedCredError instanceof Error - ? sharedCredError.message - : "Unknown error", - }, - ); } + + if (resolution.source === "secretless") { + return { + ...recipientHost, + authOverrides: { + ssh: { + required: false, + ownerAuthShared: !!host.shareSshAuth, + }, + }, + }; + } + } catch { + // A missing/deleted override or snapshot behaves like unavailable auth. } + return recipientHost; + } + + if (host.credentialId && (host.userId || host.ownerId)) { + const credentialId = host.credentialId as number; + const credentialOwnerId = (host.ownerId || host.userId) as string; + const credential = - await createCurrentHostResolutionRepository().findCredentialByIdForUser( + preloadedCredentials?.get(credentialId) ?? + (await createCurrentHostResolutionRepository().findCredentialByIdForUser( credentialId, - ownerId, - ); + credentialOwnerId, + )); if (credential) { const resolvedHost: Record = { @@ -2173,7 +2597,7 @@ async function resolveHostCredentials( return { ...host }; } catch (error) { sshLogger.warn( - `Failed to resolve credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to resolve credentials for host ${host.id}: ${getErrorMessage(error)}`, ); return host; } diff --git a/src/backend/database/routes/keybinding-validation.ts b/src/backend/database/routes/keybinding-validation.ts new file mode 100644 index 0000000..1890c32 --- /dev/null +++ b/src/backend/database/routes/keybinding-validation.ts @@ -0,0 +1,49 @@ +const VALID_ACTION_TYPES = [ + "copy", + "paste", + "sendControlCode", + "sendText", + "runSnippet", +]; + +export function isValidKeyCombo(combo: unknown): boolean { + return ( + !!combo && + typeof combo === "object" && + typeof (combo as { key?: unknown }).key === "string" && + typeof (combo as { isCode?: unknown }).isCode === "boolean" && + typeof (combo as { ctrl?: unknown }).ctrl === "boolean" && + typeof (combo as { alt?: unknown }).alt === "boolean" && + typeof (combo as { shift?: unknown }).shift === "boolean" && + typeof (combo as { meta?: unknown }).meta === "boolean" + ); +} + +export function isValidKeybindingAction(action: unknown): boolean { + if (!action || typeof action !== "object") return false; + const type = (action as { type?: unknown }).type; + if (typeof type !== "string" || !VALID_ACTION_TYPES.includes(type)) + return false; + if (type === "sendText") { + return typeof (action as { text?: unknown }).text === "string"; + } + if (type === "sendControlCode") { + const code = (action as { controlCode?: unknown }).controlCode; + return typeof code === "string" && /^[a-zA-Z]$/.test(code); + } + if (type === "runSnippet") { + return typeof (action as { snippetId?: unknown }).snippetId === "string"; + } + return true; +} + +export function isValidKeybinding(entry: unknown): boolean { + return ( + !!entry && + typeof entry === "object" && + typeof (entry as { id?: unknown }).id === "string" && + typeof (entry as { enabled?: unknown }).enabled === "boolean" && + isValidKeyCombo((entry as { combo?: unknown }).combo) && + isValidKeybindingAction((entry as { action?: unknown }).action) + ); +} diff --git a/src/backend/database/routes/open-tabs.ts b/src/backend/database/routes/open-tabs.ts index 6ea7c22..070e5f4 100644 --- a/src/backend/database/routes/open-tabs.ts +++ b/src/backend/database/routes/open-tabs.ts @@ -1,12 +1,12 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { sessionManager } from "../../hosts/terminal/session-manager.js"; import { getCurrentSettingValue, createCurrentOpenTabRepository, + createCurrentSessionShareRepository, } from "../repositories/factory.js"; const router = express.Router(); @@ -212,6 +212,7 @@ router.patch("/:id", authenticateJWT, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; const id = String(req.params.id); const updates = req.body as Partial<{ + hostId: number | null; label: string; tabOrder: number; backendSessionId: string | null; @@ -277,12 +278,15 @@ router.delete("/:id", authenticateJWT, async (req: Request, res: Response) => { * /open-tabs/active-sessions: * get: * summary: Get all active backend sessions for the current user - * description: Returns live terminal sessions from the session manager. Used by the Active Connections panel and tab restore logic. + * description: > + * Returns live terminal sessions from the session manager, both sessions the + * caller owns and SSH sessions shared to the caller by another user (via + * an in-app session share). Used by the Active Connections panel and tab restore logic. * tags: * - Open Tabs * responses: * 200: - * description: List of active sessions. + * description: List of active sessions (own and shared-with-me). * content: * application/json: * schema: @@ -302,6 +306,17 @@ router.delete("/:id", authenticateJWT, async (req: Request, res: Response) => { * type: boolean * createdAt: * type: number + * isOwnSession: + * type: boolean + * sharedByUsername: + * type: string + * nullable: true + * permissionLevel: + * type: string + * nullable: true + * shareId: + * type: string + * nullable: true */ router.get( "/active-sessions", @@ -309,17 +324,46 @@ router.get( async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; try { - const sessions = sessionManager.getUserSessions(userId); - return res.json( - sessions.map((s) => ({ - sessionId: s.id, - hostId: s.hostId, - hostName: s.hostName, - tabInstanceId: s.attachedTabInstanceId ?? s.tabInstanceId ?? null, - isConnected: s.isConnected, - createdAt: s.createdAt, - })), - ); + const ownSessions = sessionManager.getUserSessions(userId); + const result = ownSessions.map((s) => ({ + sessionId: s.id, + hostId: s.hostId, + hostName: s.hostName, + tabInstanceId: s.attachedTabInstanceId ?? s.tabInstanceId ?? null, + isConnected: s.isConnected, + createdAt: s.createdAt, + isOwnSession: true, + sharedByUsername: null as string | null, + permissionLevel: null as string | null, + shareId: null as string | null, + })); + + const sharedWithMe = + await createCurrentSessionShareRepository().findSharesTargetingUser( + userId, + ); + for (const share of sharedWithMe) { + if (share.protocol !== "ssh") continue; + const sharedSession = sessionManager.getSession(share.sessionId); + if (!sharedSession || !sharedSession.isConnected) continue; + result.push({ + sessionId: sharedSession.id, + hostId: sharedSession.hostId, + hostName: sharedSession.hostName, + tabInstanceId: + sharedSession.attachedTabInstanceId ?? + sharedSession.tabInstanceId ?? + null, + isConnected: sharedSession.isConnected, + createdAt: sharedSession.createdAt, + isOwnSession: false, + sharedByUsername: share.ownerUsername, + permissionLevel: share.permissionLevel, + shareId: share.id, + }); + } + + return res.json(result); } catch (e) { databaseLogger.error("Failed to get active sessions", e, { operation: "get_active_sessions", diff --git a/src/backend/database/routes/proxmox-import-auth.ts b/src/backend/database/routes/proxmox-import-auth.ts new file mode 100644 index 0000000..00306c8 --- /dev/null +++ b/src/backend/database/routes/proxmox-import-auth.ts @@ -0,0 +1,44 @@ +// Pure decision: which auth settings an imported Proxmox guest inherits. +// +// The frontend carries a parallel copy in +// src/ui/components/proxmox/proxmox-import-auth.ts. The two drifting apart is +// what produced the reported import bug, so both are kept behaviourally +// identical and each is unit-tested against the same matrix. +export function resolveProxmoxImportAuth( + defaultAuthType: string | undefined, + credentialId: number | null | undefined, +): { + authType: string; + credentialId: number | null; + overrideCredentialUsername: number; +} { + // An explicit special auth type (none/opkssh/tailscale/vault/โ€ฆ) wins. + if ( + defaultAuthType && + defaultAuthType !== "credential" && + !["password", "key"].includes(defaultAuthType) + ) { + return { + authType: defaultAuthType, + credentialId: null, + overrideCredentialUsername: 0, + }; + } + + // A credential (configured default OR inherited from the source host) is a + // concrete auth source -> use it, even when defaultAuthType is the + // "password"/"key" default. + if (credentialId) { + return { + authType: "credential", + credentialId, + overrideCredentialUsername: 1, + }; + } + + return { + authType: "none", + credentialId: null, + overrideCredentialUsername: 0, + }; +} diff --git a/src/backend/database/routes/proxmox.ts b/src/backend/database/routes/proxmox.ts index c3a4b8f..3dcfc5e 100644 --- a/src/backend/database/routes/proxmox.ts +++ b/src/backend/database/routes/proxmox.ts @@ -1,15 +1,19 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import express from "express"; import { Client as SSHClient } from "ssh2"; import { logger } from "../../utils/logger.js"; import { DataCrypto } from "../../utils/data-crypto.js"; -import { - createCurrentCredentialRepository, - createCurrentHostRepository, -} from "../repositories/factory.js"; +import { createCurrentHostRepository } from "../repositories/factory.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import type { SSHHost } from "../../../types/index.js"; +import { + type AuthenticatedRequest, + type SSHHost, +} from "../../../types/index.js"; import { SSHHostKeyVerifier } from "../../hosts/host-key-verifier.js"; +import { resolveHostById } from "../../hosts/host-resolver.js"; +import { createJumpHostChain } from "../../hosts/jump-host-chain.js"; +import { resolveProxmoxImportAuth } from "./proxmox-import-auth.js"; +import { isSafeNodeName } from "../../hosts/proxmox-shared.js"; const router = express.Router(); const proxmoxLogger = logger; @@ -24,18 +28,10 @@ const requireDataAccess = authManager.createDataAccessMiddleware(); // Helpers -// Proxmox node names are restricted to [a-zA-Z0-9-] by PVE itself, -// but we validate defensively before using in a shell command. -const SAFE_NODE_RE = /^[a-zA-Z0-9._-]{1,64}$/; - -function isSafeNodeName(name: string): boolean { - return SAFE_NODE_RE.test(name); -} - function execCommand( client: SSHClient, command: string, - timeoutMs = 8000, + timeoutMs = 25000, ): Promise { return new Promise((resolve, reject) => { let settled = false; @@ -187,6 +183,20 @@ type ProxmoxSyncResult = { errors: string[]; }; +function parseJumpHostsField(raw: unknown): unknown[] | null { + if (!raw) return null; + if (Array.isArray(raw)) return raw; + if (typeof raw === "string") { + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } + } + return null; +} + function parseJsonObject(value: unknown): Record { if (!value) return {}; if (typeof value === "object") return value as Record; @@ -226,6 +236,17 @@ function guestSourceKey(sourceHostId: number, guest: ProxmoxGuest): string { return `${sourceHostId}:${guest.node}:${guest.type}:${guest.vmid}`; } +function guestTags(guest: ProxmoxGuest): string[] { + const idTag = guest.type === "lxc" ? `ct-${guest.vmid}` : `vm-${guest.vmid}`; + return [ + "proxmox", + guest.type, + guest.node, + idTag, + ...(guest.enableDocker ? ["docker"] : []), + ]; +} + function mergeTags( existing: unknown, additions: string[], @@ -248,43 +269,16 @@ function mergeTags( .join(","); } -function resolveProxmoxImportAuth( - defaultAuthType: string | undefined, - credentialId: number | null | undefined, -): { - authType: string; - credentialId: number | null; - overrideCredentialUsername: number; -} { - if (defaultAuthType === "credential" || (!defaultAuthType && credentialId)) { - return credentialId - ? { authType: "credential", credentialId, overrideCredentialUsername: 1 } - : { authType: "none", credentialId: null, overrideCredentialUsername: 0 }; - } - - if (defaultAuthType && !["password", "key"].includes(defaultAuthType)) { - return { - authType: defaultAuthType, - credentialId: null, - overrideCredentialUsername: 0, - }; - } - - return { - authType: "none", - credentialId: null, - overrideCredentialUsername: 0, - }; -} - async function discoverProxmoxGuestsForHost( userId: string, parsedHostId: number, + onProgress?: (done: number, total: number) => void, ): Promise<{ host: SSHHost; guests: ProxmoxGuest[]; credentialId: number | null; defaultCredentialId: number | null; + jumpHosts: unknown[] | null; config: ReturnType; }> { if (!DataCrypto.canUserAccessData(userId)) { @@ -293,34 +287,18 @@ async function discoverProxmoxGuestsForHost( throw error; } - const hostRecord = await createCurrentHostRepository().findDecryptedByIdAs( - userId, - parsedHostId, - ); - - if (!hostRecord) { + const resolvedHost = await resolveHostById(parsedHostId, userId); + if (!resolvedHost) { const error = new Error("Host not found"); (error as Error & { status?: number }).status = 404; throw error; } - const host = hostRecord as unknown as SSHHost; + const host = resolvedHost as SSHHost; const proxmoxCfgRaw = parseJsonObject(host.proxmoxConfig); const config = parseProxmoxConfig(proxmoxCfgRaw); - if (host.userId !== userId) { - const { PermissionManager } = - await import("../../utils/permission-manager.js"); - const pm = PermissionManager.getInstance(); - const access = await pm.canAccessHost(userId, parsedHostId, "connect"); - if (!access.hasAccess) { - const error = new Error("Access denied"); - (error as Error & { status?: number }).status = 403; - throw error; - } - } - - let resolvedCredentials: { + const resolvedCredentials: { password?: string; sshKey?: string; keyPassword?: string; @@ -334,50 +312,6 @@ async function discoverProxmoxGuestsForHost( const hostCredentialId = host.credentialId ?? null; - if (host.credentialId) { - if (userId !== host.userId) { - try { - const { SharedHostSecretsManager } = - await import("../../utils/shared-host-secrets-manager.js"); - const sharedCred = - await SharedHostSecretsManager.getInstance().getSecretForUser( - host.id, - userId, - "ssh", - ); - if (sharedCred) { - resolvedCredentials = { - password: sharedCred.password, - sshKey: sharedCred.key, - keyPassword: sharedCred.keyPassword, - authType: sharedCred.authType, - }; - } - } catch (err) { - proxmoxLogger.error("Failed to resolve shared credential", err, { - operation: "proxmox_discover", - hostId: parsedHostId, - userId, - }); - } - } else { - const cred = - await createCurrentCredentialRepository().findDecryptedByIdForUser( - userId, - host.credentialId as number, - ); - if (cred) { - const c = cred; - resolvedCredentials = { - password: c.password as string | undefined, - sshKey: (c.key || c.privateKey) as string | undefined, - keyPassword: c.keyPassword as string | undefined, - authType: c.authType as string | undefined, - }; - } - } - } - const sshConfig: Record = { host: host.ip?.replace(/^\[|\]$/g, "") || host.ip, port: host.port || 22, @@ -420,7 +354,51 @@ async function discoverProxmoxGuestsForHost( await new Promise((resolve, reject) => { client.on("ready", resolve); client.on("error", reject); - client.connect(sshConfig as import("ssh2").ConnectConfig); + + // Reuse the shared jump-host chain (same path terminal/metrics use) + // so Proxmox hosts that are only reachable via a jump host work too + // (otherwise the direct connect fails with EHOSTUNREACH). jumpHosts is + // stored as a JSON string on the decrypted record, so parse it first. + let parsedJumpHosts: Array<{ hostId: number }> = []; + try { + const rawJumpHosts = (host as { jumpHosts?: unknown }).jumpHosts; + const parsed = + typeof rawJumpHosts === "string" + ? JSON.parse(rawJumpHosts) + : rawJumpHosts; + if (Array.isArray(parsed)) parsedJumpHosts = parsed; + } catch { + parsedJumpHosts = []; + } + + if (parsedJumpHosts.length > 0) { + createJumpHostChain(parsedJumpHosts, userId) + .then((jumpClient) => { + if (!jumpClient) { + reject(new Error("Jump host chain could not be established")); + return; + } + jumpClient.forwardOut( + "127.0.0.1", + 0, + sshConfig.host as string, + sshConfig.port as number, + (err, stream) => { + if (err || !stream) { + reject(err || new Error("Jump host forward failed")); + return; + } + sshConfig.sock = stream; + delete sshConfig.host; + delete sshConfig.port; + client.connect(sshConfig as import("ssh2").ConnectConfig); + }, + ); + }) + .catch(reject); + } else { + client.connect(sshConfig as import("ssh2").ConnectConfig); + } }); proxmoxLogger.info("Proxmox discovery SSH connection established", { @@ -488,23 +466,58 @@ async function discoverProxmoxGuestsForHost( async function resolveIp(g: GuestBase): Promise { if (g.type === "lxc") { + let configIp: string | null = null; try { const cfgJson = await execCommand( client, `pvesh get /nodes/${g.node}/lxc/${g.vmid}/config --output-format json 2>/dev/null`, - 8000, + 25000, ); - return parseLxcIp(JSON.parse(cfgJson), config.preferredPrefixes); + configIp = parseLxcIp(JSON.parse(cfgJson), config.preferredPrefixes); } catch { - return null; + configIp = null; } + if (configIp) return configIp; + // Static config parsing found nothing (e.g. net0 uses ip=dhcp). + // Fall back to the live interface list for running containers. + if (g.status === "running") { + try { + const ifRaw = await execCommand( + client, + `pvesh get /nodes/${g.node}/lxc/${g.vmid}/interfaces --output-format json 2>/dev/null`, + 12000, + ); + const data = JSON.parse(ifRaw); + const entries: Array> = Array.isArray(data) + ? data + : []; + const allIps: string[] = []; + for (const entry of entries) { + if (entry.name === "lo") continue; + const inet = entry.inet; + if (typeof inet !== "string") continue; + const m = inet.match(/^(\d{1,3}(?:\.\d{1,3}){3})\/\d+$/); + if (m && !m[1].startsWith("127.")) allIps.push(m[1]); + } + if (allIps.length) { + for (const prefix of config.preferredPrefixes) { + const match = allIps.find((ip) => ip.startsWith(prefix)); + if (match) return match; + } + return allIps[0]; + } + } catch { + // Guest not running or interfaces unavailable + } + } + return null; } if (g.type === "qemu" && g.status === "running") { try { const ifJson = await execCommand( client, `pvesh get /nodes/${g.node}/qemu/${g.vmid}/agent/network-get-interfaces --output-format json 2>/dev/null`, - 5000, + 12000, ); const data = JSON.parse(ifJson); const ifaces: Array> = Array.isArray( @@ -542,13 +555,21 @@ async function discoverProxmoxGuestsForHost( return null; } - const CONCURRENCY = 6; + // Low concurrency on purpose: pvesh is heavy and small Proxmox nodes + // (especially reached over a high-latency jump chain) suffer severe + // contention when many run at once โ€” calls then exceed execCommand's + // timeout and IPs come back empty. 2 keeps each call well under budget. + const CONCURRENCY = 2; const ips: (string | null)[] = new Array(guestBases.length).fill(null); let cursor = 0; + let completed = 0; + onProgress?.(0, guestBases.length); async function ipWorker() { while (cursor < guestBases.length) { const i = cursor++; ips[i] = await resolveIp(guestBases[i]); + completed++; + onProgress?.(completed, guestBases.length); } } await Promise.all( @@ -578,6 +599,9 @@ async function discoverProxmoxGuestsForHost( guests, credentialId: hostCredentialId, defaultCredentialId: config.defaultCredentialId, + jumpHosts: parseJumpHostsField( + (host as unknown as { jumpHosts?: unknown }).jumpHosts, + ), config, }; } finally { @@ -653,14 +677,6 @@ async function syncProxmoxHost( missingSince: null, }; - if (!existing && !guest.ip) { - result.skipped++; - result.errors.push( - `${guest.name}: skipped because no IP address was discovered`, - ); - continue; - } - const baseConfig = existing ? parseJsonObject(existing.proxmoxConfig) : {}; @@ -683,20 +699,16 @@ async function syncProxmoxHost( typeof existing?.username === "string" && existing.username ? existing.username : connectionType === "rdp" - ? null + ? "" : "root"; const update: Record = { name: guest.name, - ip: guest.ip || existing?.ip, + ip: guest.ip || existing?.ip || "0.0.0.0", port, username, connectionType, folder: existing?.folder || sourceHostName, - tags: mergeTags( - existing?.tags, - ["proxmox", guest.type, guest.node], - ["proxmox-missing"], - ), + tags: mergeTags(existing?.tags, guestTags(guest), ["proxmox-missing"]), proxmoxConfig: JSON.stringify(proxmoxConfig), updatedAt: now, }; @@ -746,7 +758,9 @@ async function syncProxmoxHost( telnetPort: null, defaultPath: "/", tunnelConnections: "[]", - jumpHosts: null, + jumpHosts: + (discovery.host as unknown as { jumpHosts?: string | null }) + .jumpHosts ?? null, quickActions: null, statsConfig: null, dockerConfig: null, @@ -803,7 +817,7 @@ async function syncProxmoxHost( return result; } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; + const message = getErrorMessage(error); result.errors.push(message); await writeSyncStatus(userId, sourceHostId, { lastSyncAt: startedAt, @@ -844,7 +858,7 @@ router.post("/sync", authenticateJWT, requireDataAccess, async (req, res) => { const result = await syncProxmoxHost(userId, parsedHostId); return res.json(result); } catch (err: unknown) { - const message = err instanceof Error ? err.message : "Unknown error"; + const message = getErrorMessage(err); const status = (err as Error & { code?: string; status?: number }).code === "SESSION_EXPIRED" @@ -975,6 +989,81 @@ proxmoxAutoSyncStartupTimer.unref?.(); * 500: * description: Discovery failed. */ +router.get( + "/discover/stream", + authenticateJWT, + requireDataAccess, + async (req, res) => { + const userId = (req as unknown as AuthenticatedRequest).userId; + const parsedHostId = Number((req.query as { hostId?: unknown }).hostId); + if (!parsedHostId || !Number.isInteger(parsedHostId) || parsedHostId <= 0) { + return res.status(400).json({ error: "Missing or invalid hostId" }); + } + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-store, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }); + res.flushHeaders?.(); + + let closed = false; + const send = (event: string, data: unknown) => { + if (closed) return; + try { + res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + } catch { + closed = true; + } + }; + const heartbeat = setInterval(() => { + if (closed) return; + try { + res.write(": keepalive\n\n"); + } catch { + closed = true; + clearInterval(heartbeat); + } + }, 15000); + req.on("close", () => { + closed = true; + clearInterval(heartbeat); + }); + + try { + const discovery = await discoverProxmoxGuestsForHost( + userId, + parsedHostId, + (done, total) => send("progress", { done, total }), + ); + send("result", { + guests: discovery.guests, + credentialId: discovery.credentialId, + defaultCredentialId: discovery.defaultCredentialId, + jumpHosts: discovery.jumpHosts, + }); + } catch (err: unknown) { + const message = getErrorMessage(err); + proxmoxLogger.error("Proxmox discovery (stream) failed", err, { + operation: "proxmox_discover", + hostId: parsedHostId, + userId, + }); + send("fail", { message }); + } finally { + clearInterval(heartbeat); + if (!closed) { + try { + res.end(); + } catch { + // ignore end errors + } + } + } + }, +); + router.post( "/discover", authenticateJWT, @@ -997,9 +1086,10 @@ router.post( guests: discovery.guests, credentialId: discovery.credentialId, defaultCredentialId: discovery.defaultCredentialId, + jumpHosts: discovery.jumpHosts, }); } catch (err: unknown) { - const message = err instanceof Error ? err.message : "Unknown error"; + const message = getErrorMessage(err); proxmoxLogger.error("Proxmox discovery failed", err, { operation: "proxmox_discover", hostId: parsedHostId, diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts index 06fbb12..b8af050 100644 --- a/src/backend/database/routes/rbac.ts +++ b/src/backend/database/routes/rbac.ts @@ -1,8 +1,14 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Response } from "express"; +import express, { type Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; +import { getRequestMeta } from "../../utils/audit-logger.js"; +import { isAuthOverrideProtocol } from "../../../types/auth-protocols.js"; +import { + SharedHostAuthOverrideService, + SharedHostAuthOverrideServiceError, +} from "../../utils/shared-host-auth-override-service.js"; import { PermissionManager, SHARE_PERMISSION_LEVELS, @@ -13,7 +19,7 @@ import { isValidPermission, } from "../../utils/permission-catalog.js"; import { - createCurrentCredentialRepository, + createCurrentHostFolderRepository, createCurrentHostResolutionRepository, createCurrentRbacAccessRepository, createCurrentRoleRepository, @@ -27,16 +33,21 @@ const authManager = AuthManager.getInstance(); const permissionManager = PermissionManager.getInstance(); const authenticateJWT = authManager.createAuthMiddleware(); +const requireDataAccess = authManager.createDataAccessMiddleware(); +const sharedHostAuthOverrideService = + SharedHostAuthOverrideService.getInstance(); function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } -function isSharePermissionLevel(value: unknown): value is SharePermissionLevel { +export function isSharePermissionLevel( + value: unknown, +): value is SharePermissionLevel { return SHARE_PERMISSION_LEVELS.includes(value as SharePermissionLevel); } -function expiryFromDuration(durationHours: unknown): string | null { +export function expiryFromDuration(durationHours: unknown): string | null { if (durationHours && typeof durationHours === "number" && durationHours > 0) { const expiryDate = new Date(); expiryDate.setTime(expiryDate.getTime() + durationHours * 60 * 60 * 1000); @@ -58,12 +69,12 @@ async function canManageHostSharing( return { allowed: access.hasAccess, isOwner: access.isOwner }; } -interface ShareTarget { +export interface ShareTarget { type: "user" | "role"; id: string | number; } -function parseShareTargets( +export function parseShareTargets( body: Record, ): ShareTarget[] | null { const rawTargets = body.targets; @@ -94,7 +105,7 @@ function parseShareTargets( * /rbac/host/{id}/share: * post: * summary: Share a host - * description: Shares a host with one or more users and/or roles at a permission level (connect, view, edit, manage). Allowed for the host owner or recipients holding the manage level. Every auth type is shareable; per-recipient secret snapshots are created automatically. + * description: Shares a host with one or more users and/or roles at a permission level (connect, view, edit, manage). SSH authentication remains private to the owner; recipients may select one of their own saved SSH credentials. * tags: * - RBAC * parameters: @@ -270,10 +281,7 @@ router.post( operation: "rbac_host_share_snapshot_failed", hostId, accessId: accessGrant.id, - error: - snapshotError instanceof Error - ? snapshotError.message - : "Unknown error", + error: getErrorMessage(snapshotError), }); } @@ -311,6 +319,217 @@ router.post( }, ); +/** + * @openapi + * /rbac/folder/share: + * post: + * summary: Share all hosts in a folder + * description: Shares every host within a folder (and its subfolders) with one or more users and/or roles at a permission level. Only hosts owned by the caller are shared; skips hosts the caller may not share. + * tags: + * - RBAC + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [folder, targets] + * properties: + * folder: + * type: string + * targets: + * type: array + * items: + * type: object + * properties: + * type: + * type: string + * enum: [user, role] + * id: + * oneOf: + * - type: string + * - type: integer + * permissionLevel: + * type: string + * enum: [connect, view, edit, manage] + * durationHours: + * type: number + * responses: + * 200: + * description: Folder shared successfully. + * 400: + * description: Invalid request body. + * 500: + * description: Failed to share folder. + */ +router.post( + "/folder/share", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const userId = req.userId!; + const { folder } = req.body ?? {}; + + if (!isNonEmptyString(folder)) { + return res.status(400).json({ error: "Folder name is required" }); + } + + try { + const targets = parseShareTargets(req.body ?? {}); + if (!targets) { + return res.status(400).json({ + error: + "targets must be a non-empty array of { type: 'user'|'role', id } entries", + }); + } + + const { durationHours, permissionLevel = "connect" } = req.body; + + if (!isSharePermissionLevel(permissionLevel)) { + return res.status(400).json({ + error: "Invalid permission level", + validLevels: SHARE_PERMISSION_LEVELS, + }); + } + + const userRepository = createCurrentUserRepository(); + const roleRepository = createCurrentRoleRepository(); + for (const target of targets) { + if (target.type === "user") { + const targetUser = await userRepository.findById(target.id as string); + if (!targetUser) { + return res.status(404).json({ + error: "Target user not found", + targetId: target.id, + }); + } + } else { + const targetRole = await roleRepository.findRoleById( + target.id as number, + ); + if (!targetRole) { + return res.status(404).json({ + error: "Target role not found", + targetId: target.id, + }); + } + } + } + + const hostsInFolder = + await createCurrentHostFolderRepository().listHostsInFolder( + userId, + folder, + ); + + const expiresAt = expiryFromDuration(durationHours); + const rbacAccessRepository = createCurrentRbacAccessRepository(); + const { SharedHostSecretsManager } = + await import("../../utils/shared-host-secrets-manager.js"); + const secretsManager = SharedHostSecretsManager.getInstance(); + + const hostResults: Array<{ + hostId: number; + shared: boolean; + reason?: string; + }> = []; + + for (const host of hostsInFolder) { + if (targets.some((t) => t.type === "user" && t.id === host.userId)) { + hostResults.push({ + hostId: host.id, + shared: false, + reason: "owner", + }); + continue; + } + + const sharing = await canManageHostSharing(userId, host.id); + if (!sharing.allowed) { + hostResults.push({ + hostId: host.id, + shared: false, + reason: "forbidden", + }); + continue; + } + + for (const target of targets) { + const accessGrant = await rbacAccessRepository.upsertHostAccess({ + hostId: host.id, + grantedBy: userId, + permissionLevel, + expiresAt, + ...(target.type === "user" + ? { + targetType: "user" as const, + targetUserId: target.id as string, + } + : { + targetType: "role" as const, + targetRoleId: target.id as number, + }), + }); + + try { + if (target.type === "user") { + await secretsManager.snapshotForUser( + accessGrant.id, + host.id, + target.id as string, + host.userId, + ); + } else { + await secretsManager.snapshotForRole( + accessGrant.id, + host.id, + target.id as number, + host.userId, + ); + } + } catch (snapshotError) { + databaseLogger.warn("Share created but secret snapshot failed", { + operation: "rbac_folder_share_snapshot_failed", + hostId: host.id, + accessId: accessGrant.id, + error: getErrorMessage(snapshotError), + }); + } + } + + hostResults.push({ hostId: host.id, shared: true }); + } + + const sharedCount = hostResults.filter((r) => r.shared).length; + + databaseLogger.success("Folder shared successfully", { + operation: "rbac_folder_share_success", + userId, + folder, + hostsShared: sharedCount, + targets: targets.length, + permissionLevel, + }); + + res.json({ + success: true, + message: "Folder shared successfully", + permissionLevel, + expiresAt, + hostsShared: sharedCount, + hostsTotal: hostsInFolder.length, + hostResults, + }); + } catch (error) { + databaseLogger.error("Failed to share folder", error, { + operation: "share_folder", + folder, + userId, + }); + res.status(500).json({ error: "Failed to share folder" }); + } + }, +); + /** * @openapi * /rbac/host/{id}/access/{accessId}: @@ -1210,10 +1429,7 @@ router.delete( operation: "remove_role_secret_cleanup", targetUserId, roleId, - error: - cleanupError instanceof Error - ? cleanupError.message - : "Unknown error", + error: getErrorMessage(cleanupError), }, ); } @@ -1545,50 +1761,106 @@ router.get( }, ); +/** + * @openapi + * /rbac/host-access/{hostId}/auth/{protocol}: + * put: + * summary: Set personal authentication for a shared host protocol + * description: Selects one of the authenticated recipient's own credentials, or clears the selection with null. Only SSH is currently supported. + * tags: [RBAC] + * security: + * - bearerAuth: [] + */ router.put( - "/host-access/:hostId/credential", + "/host-access/:hostId/auth/:protocol", + authenticateJWT, + requireDataAccess, async (req: express.Request, res: express.Response) => { try { const userId = (req as AuthenticatedRequest).userId!; const hostId = Number.parseInt(String(req.params.hostId), 10); + const protocol = req.params.protocol; const { credentialId } = req.body; - if (!hostId || isNaN(hostId)) { + if (!Number.isInteger(hostId) || hostId <= 0) { return res.status(400).json({ error: "Invalid host ID" }); } - - const access = - await createCurrentRbacAccessRepository().findDirectHostAccess( - hostId, - userId, - ); - - if (!access) { - return res.status(403).json({ error: "No access to this host" }); + if (!isAuthOverrideProtocol(protocol)) { + return res + .status(400) + .json({ error: "Invalid authentication protocol" }); } - if (credentialId) { - const cred = await createCurrentCredentialRepository().findByIdForUser( - userId, - credentialId, - ); - - if (!cred) { - return res.status(404).json({ error: "Credential not found" }); - } + if ( + credentialId !== null && + (!Number.isInteger(credentialId) || credentialId <= 0) + ) { + return res.status(400).json({ + error: "credentialId must be a positive integer or null", + }); } - await createCurrentRbacAccessRepository().updateHostAccessOverrideCredential( - access.id, - credentialId || null, + const { ipAddress, userAgent } = getRequestMeta(req); + await sharedHostAuthOverrideService.setCredentialId( + hostId, + userId, + protocol, + credentialId, + { ipAddress, userAgent }, ); - - res.json({ success: true }); + res.json({ success: true, protocol, credentialId }); } catch (error) { + if (error instanceof SharedHostAuthOverrideServiceError) { + return res.status(error.statusCode).json({ error: error.message }); + } databaseLogger.error("Failed to set override credential", error); res.status(500).json({ error: "Failed to update credential" }); } }, ); +/** + * @openapi + * /rbac/host-access/{hostId}/auth/{protocol}: + * get: + * summary: Get the current recipient's shared-host protocol authentication override + * tags: [RBAC] + * security: + * - bearerAuth: [] + */ +router.get( + "/host-access/:hostId/auth/:protocol", + authenticateJWT, + requireDataAccess, + async (req: express.Request, res: express.Response) => { + try { + const userId = (req as AuthenticatedRequest).userId!; + const hostId = Number.parseInt(String(req.params.hostId), 10); + const protocol = req.params.protocol; + + if (!Number.isInteger(hostId) || hostId <= 0) { + return res.status(400).json({ error: "Invalid host ID" }); + } + if (!isAuthOverrideProtocol(protocol)) { + return res + .status(400) + .json({ error: "Invalid authentication protocol" }); + } + + const credentialId = await sharedHostAuthOverrideService.getCredentialId( + hostId, + userId, + protocol, + ); + res.json({ protocol, credentialId }); + } catch (error) { + if (error instanceof SharedHostAuthOverrideServiceError) { + return res.status(error.statusCode).json({ error: error.message }); + } + databaseLogger.error("Failed to get override credential", error); + res.status(500).json({ error: "Failed to fetch credential" }); + } + }, +); + export default router; diff --git a/src/backend/database/routes/session-log-routes.ts b/src/backend/database/routes/session-log-routes.ts index 3873aa8..f1b5e1d 100644 --- a/src/backend/database/routes/session-log-routes.ts +++ b/src/backend/database/routes/session-log-routes.ts @@ -1,10 +1,9 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; +import express, { type Request, type Response } from "express"; import fs from "fs"; import path from "path"; import { apiLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import type { Request, Response } from "express"; import { PermissionManager } from "../../utils/permission-manager.js"; import { createCurrentSessionRecordingRepository, diff --git a/src/backend/database/routes/snippets-execution.ts b/src/backend/database/routes/snippets-execution.ts new file mode 100644 index 0000000..99da7c0 --- /dev/null +++ b/src/backend/database/routes/snippets-execution.ts @@ -0,0 +1,82 @@ +export interface SnippetExecutionResult { + success: boolean; + output: string; + error?: string; +} + +export interface SnippetHostVars { + ip?: string; + username?: string; + port?: number | string; + name?: string; +} + +const INPUT_PATTERN = + /\$\{INPUT_(\d+)(?::([^}$]+))?\}|\$INPUT_(\d+)(?![a-zA-Z0-9_])/g; + +function replaceVar(content: string, name: string, value?: string): string { + if (value === undefined) return content; + const pattern = new RegExp(`\\$\\{?${name}\\}?`, "g"); + return content.replace(pattern, value); +} + +/** + * Mirrors src/ui/lib/snippet-variables.ts resolveSnippetContent. Frontend and + * backend are separate builds, so this is kept as a small standalone copy + * rather than a shared package for one pure function. + */ +export function resolveSnippetCommand( + content: string, + host: SnippetHostVars | null, + inputValues: Record = {}, +): string { + let resolved = content; + + resolved = replaceVar(resolved, "HOST", host?.ip); + resolved = replaceVar(resolved, "USER", host?.username); + resolved = replaceVar( + resolved, + "PORT", + host?.port !== undefined ? String(host.port) : undefined, + ); + resolved = replaceVar(resolved, "NAME", host?.name); + + resolved = resolved.replace( + INPUT_PATTERN, + ( + fullMatch, + braceDigits: string | undefined, + _label, + plainDigits: string | undefined, + ) => { + const key = `INPUT_${braceDigits ?? plainDigits}`; + return key in inputValues ? inputValues[key] : fullMatch; + }, + ); + + return resolved; +} + +export function getSnippetExecutionTimeoutMs( + value = process.env.SNIPPET_EXECUTION_TIMEOUT_SECONDS, +): number | undefined { + if (value === undefined || value.trim() === "") return undefined; + + const seconds = Number(value); + if (!Number.isFinite(seconds) || seconds <= 0) return undefined; + + return seconds * 1000; +} + +export function createSnippetExecutionResult( + exitCode: number | null, + output: string, + errorOutput: string, +): SnippetExecutionResult { + const success = exitCode === 0 || (exitCode === null && !errorOutput); + return { + success, + output, + ...(errorOutput ? { error: errorOutput } : {}), + }; +} diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts index 2fbc406..3f0e7b1 100644 --- a/src/backend/database/routes/snippets.ts +++ b/src/backend/database/routes/snippets.ts @@ -1,10 +1,15 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js"; import { extractSnippetReorderUpdates } from "./snippets-reorder.js"; +import { + createSnippetExecutionResult, + getSnippetExecutionTimeoutMs, + resolveSnippetCommand, +} from "./snippets-execution.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; import { createCurrentHostResolutionRepository, @@ -12,6 +17,7 @@ import { createCurrentRoleRepository, createCurrentSnippetRepository, createCurrentUserRepository, + createCurrentSyncTombstoneRepository, } from "../repositories/factory.js"; const router = express.Router(); @@ -176,10 +182,7 @@ router.post( } catch (err) { authLogger.error("Failed to create snippet folder", err); res.status(500).json({ - error: - err instanceof Error - ? err.message - : "Failed to create snippet folder", + error: getErrorMessage(err, "Failed to create snippet folder"), }); } }, @@ -263,10 +266,7 @@ router.put( } catch (err) { authLogger.error("Failed to update snippet folder metadata", err); res.status(500).json({ - error: - err instanceof Error - ? err.message - : "Failed to update snippet folder metadata", + error: getErrorMessage(err, "Failed to update snippet folder metadata"), }); } }, @@ -351,10 +351,7 @@ router.put( } catch (err) { authLogger.error("Failed to rename snippet folder", err); res.status(500).json({ - error: - err instanceof Error - ? err.message - : "Failed to rename snippet folder", + error: getErrorMessage(err, "Failed to rename snippet folder"), }); } }, @@ -400,7 +397,17 @@ router.delete( try { const folderName = decodeURIComponent(name); - await createCurrentSnippetRepository().deleteFolder(userId, folderName); + const deletedFolder = await createCurrentSnippetRepository().deleteFolder( + userId, + folderName, + ); + if (deletedFolder?.syncId) { + await createCurrentSyncTombstoneRepository().record( + userId, + "snippetFolders", + deletedFolder.syncId, + ); + } authLogger.success( `Snippet folder deleted: ${folderName} by user ${userId}`, @@ -415,10 +422,7 @@ router.delete( } catch (err) { authLogger.error("Failed to delete snippet folder", err); res.status(500).json({ - error: - err instanceof Error - ? err.message - : "Failed to delete snippet folder", + error: getErrorMessage(err, "Failed to delete snippet folder"), }); } }, @@ -498,8 +502,7 @@ router.put( } catch (err) { authLogger.error("Failed to reorder snippets", err); res.status(500).json({ - error: - err instanceof Error ? err.message : "Failed to reorder snippets", + error: getErrorMessage(err, "Failed to reorder snippets"), }); } }, @@ -524,6 +527,15 @@ router.put( * type: integer * hostId: * type: integer + * inputValues: + * type: object + * description: > + * Optional resolved values for $INPUT_n placeholders in the + * snippet content, keyed by "INPUT_n". Host variables + * ($HOST, $USER, $PORT, $NAME) are resolved server-side per + * target host and do not need to be passed here. + * additionalProperties: + * type: string * responses: * 200: * description: Snippet executed successfully. @@ -540,7 +552,7 @@ router.post( requireDataAccess, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; - const { snippetId, hostId } = req.body; + const { snippetId, hostId, inputValues } = req.body; if (!isNonEmptyString(userId) || !snippetId || !hostId) { authLogger.warn("Invalid snippet execution request", { @@ -560,6 +572,12 @@ router.post( return res.status(404).json({ error: "Snippet not found" }); } + if (snippet.isNote) { + return res + .status(400) + .json({ error: "Notes cannot be executed on a host" }); + } + const { Client } = await import("ssh2"); const repository = createCurrentHostResolutionRepository(); const host = await repository.findHostById(parseInt(hostId), userId); @@ -583,8 +601,7 @@ router.post( authType = (cred.authType || authType) as string; password = (cred.password || undefined) as string | undefined; privateKey = (cred.privateKey || cred.key || undefined) as - | string - | undefined; + string | undefined; passphrase = (cred.keyPassword || undefined) as string | undefined; } } @@ -593,32 +610,48 @@ router.post( let output = ""; let errorOutput = ""; + const resolvedCommand = resolveSnippetCommand( + snippet.content, + { + ip: host.ip, + username: host.username, + port: host.port, + name: host.name, + }, + inputValues && typeof inputValues === "object" ? inputValues : {}, + ); + const executePromise = new Promise<{ success: boolean; output: string; error?: string; }>((resolve, reject) => { - const timeout = setTimeout(() => { - conn.end(); - reject(new Error("Command execution timeout (30s)")); - }, 30000); + const timeoutMs = getSnippetExecutionTimeoutMs(); + let timeout: NodeJS.Timeout | undefined; conn.on("ready", () => { - conn.exec(snippet.content, (err, stream) => { + conn.exec(resolvedCommand, (err, stream) => { if (err) { clearTimeout(timeout); conn.end(); return reject(err); } - stream.on("close", () => { + if (timeoutMs) { + timeout = setTimeout(() => { + conn.end(); + reject( + new Error(`Command execution timeout (${timeoutMs / 1000}s)`), + ); + }, timeoutMs); + } + + stream.on("close", (exitCode: number | null) => { clearTimeout(timeout); conn.end(); - if (errorOutput) { - resolve({ success: false, output, error: errorOutput }); - } else { - resolve({ success: true, output }); - } + resolve( + createSnippetExecutionResult(exitCode, output, errorOutput), + ); }); stream.on("data", (data: Buffer) => { @@ -738,7 +771,7 @@ router.post( } catch (err) { authLogger.error("Failed to execute snippet", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to execute snippet", + error: getErrorMessage(err, "Failed to execute snippet"), }); } }, @@ -995,7 +1028,7 @@ router.get( } catch (err) { authLogger.error("Failed to fetch snippet", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to fetch snippet", + error: getErrorMessage(err, "Failed to fetch snippet"), }); } }, @@ -1026,6 +1059,9 @@ router.get( * type: string * order: * type: integer + * isNote: + * type: boolean + * description: When true, the snippet is a note (copy/paste only, not directly executable on a host). * responses: * 201: * description: Snippet created successfully. @@ -1040,7 +1076,8 @@ router.post( requireDataAccess, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; - const { name, content, description, folder, order, hostFilter } = req.body; + const { name, content, description, folder, order, hostFilter, isNote } = + req.body; if ( !isNonEmptyString(userId) || @@ -1066,6 +1103,7 @@ router.post( folder, order, hostFilter, + isNote, }, ); databaseLogger.info("Command snippet created", { @@ -1092,7 +1130,7 @@ router.post( } catch (err) { authLogger.error("Failed to create snippet", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to create snippet", + error: getErrorMessage(err, "Failed to create snippet"), }); } }, @@ -1129,6 +1167,9 @@ router.post( * type: string * order: * type: integer + * isNote: + * type: boolean + * description: When true, the snippet is a note (copy/paste only, not directly executable on a host). * responses: * 200: * description: The updated snippet. @@ -1187,7 +1228,7 @@ router.put( } catch (err) { authLogger.error("Failed to update snippet", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to update snippet", + error: getErrorMessage(err, "Failed to update snippet"), }); } }, @@ -1241,6 +1282,14 @@ router.delete( return res.status(404).json({ error: "Snippet not found" }); } + if (existing.syncId) { + await createCurrentSyncTombstoneRepository().record( + userId, + "snippets", + existing.syncId, + ); + } + databaseLogger.info("Command snippet deleted", { operation: "snippet_delete", userId, @@ -1264,7 +1313,7 @@ router.delete( } catch (err) { authLogger.error("Failed to delete snippet", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to delete snippet", + error: getErrorMessage(err, "Failed to delete snippet"), }); } }, diff --git a/src/backend/database/routes/sso-provider-routes.ts b/src/backend/database/routes/sso-provider-routes.ts index fc29e09..c2a5113 100644 --- a/src/backend/database/routes/sso-provider-routes.ts +++ b/src/backend/database/routes/sso-provider-routes.ts @@ -8,54 +8,44 @@ import { AuthManager } from "../../utils/auth-manager.js"; import type { SSOProviderType } from "../../../types/index.js"; import { createCurrentSsoProviderRepository } from "../repositories/factory.js"; import { getOIDCConfigFromEnv } from "./user-oidc-utils.js"; +import { + decryptSsoConfigSecrets, + encryptSsoConfigSecrets, +} from "../../utils/system-secret-crypto.js"; +import { isTrustedProxyAuthEnabled } from "../../utils/trusted-proxy-auth.js"; + +function isOidcLike(type: SSOProviderType): boolean { + return type === "oidc" || type === "github" || type === "google"; +} const authManager = AuthManager.getInstance(); -function decryptProviderConfig( +/** + * SSO secrets belong to the installation, not to a user: `sso_providers` has no + * userId and the values must be readable during login, before anyone is + * authenticated. They are encrypted with the system key rather than a user DEK. + * Values written by the previous base64 scheme still decode, and are upgraded + * the next time the provider is saved. + */ +async function decryptProviderConfig( configJson: string, _userId: string, -): Record { +): Promise> { let config: Record; try { config = JSON.parse(configJson); } catch { return {}; } - - for (const field of ["client_secret", "bindPassword"] as const) { - const val = config[field] as string | undefined; - if (val?.startsWith("encoded:")) { - try { - config[field] = Buffer.from(val.substring(8), "base64").toString( - "utf8", - ); - } catch { - config[field] = "[ENCODING ERROR]"; - } - } - } - return config; + return decryptSsoConfigSecrets(config); } -function encryptProviderConfig( +async function encryptProviderConfig( config: Record, _userId: string, _providerId: string, -): string { - const encoded: Record = { ...config }; - if ( - typeof config.client_secret === "string" && - !config.client_secret.startsWith("encoded:") - ) { - encoded.client_secret = `encoded:${Buffer.from(config.client_secret).toString("base64")}`; - } - if ( - typeof config.bindPassword === "string" && - !config.bindPassword.startsWith("encoded:") - ) { - encoded.bindPassword = `encoded:${Buffer.from(config.bindPassword).toString("base64")}`; - } - return JSON.stringify(encoded); +): Promise { + return JSON.stringify(await encryptSsoConfigSecrets(config)); } function applyProviderDefaults( @@ -141,10 +131,12 @@ export function registerSSOProviderRoutes(router: Router): void { try { const rows = await createCurrentSsoProviderRepository().listAll(); - const result = rows.map((row) => ({ - ...row, - config: decryptProviderConfig(row.config, userId), - })); + const result = await Promise.all( + rows.map(async (row) => ({ + ...row, + config: await decryptProviderConfig(row.config, userId), + })), + ); res.json(result); } catch (err) { authLogger.error("Failed to list SSO providers (admin)", err); @@ -201,6 +193,12 @@ export function registerSSOProviderRoutes(router: Router): void { if (!validTypes.includes(type)) { return res.status(400).json({ error: "Invalid provider type" }); } + if (isTrustedProxyAuthEnabled() && enabled && isOidcLike(type)) { + return res.status(409).json({ + error: + "OIDC providers cannot be enabled with trusted proxy authentication", + }); + } const configWithDefaults = type === "github" || type === "google" @@ -253,7 +251,7 @@ export function registerSSOProviderRoutes(router: Router): void { } const tempId = `new-${Date.now()}`; - const encryptedConfig = encryptProviderConfig( + const encryptedConfig = await encryptProviderConfig( configWithDefaults as Record, userId, tempId, @@ -275,7 +273,7 @@ export function registerSSOProviderRoutes(router: Router): void { }); res.status(201).json({ ...inserted, - config: decryptProviderConfig(inserted.config, userId), + config: await decryptProviderConfig(inserted.config, userId), }); } catch (err) { authLogger.error("Failed to create SSO provider", err); @@ -330,9 +328,22 @@ export function registerSSOProviderRoutes(router: Router): void { config?: Record; }; + const effectiveType = type ?? (existing.type as SSOProviderType); + const effectiveEnabled = enabled ?? existing.enabled; + if ( + isTrustedProxyAuthEnabled() && + effectiveEnabled && + isOidcLike(effectiveType) + ) { + return res.status(409).json({ + error: + "OIDC providers cannot be enabled with trusted proxy authentication", + }); + } + let encryptedConfig = existing.config; if (rawConfig !== undefined) { - const existingDecrypted = decryptProviderConfig( + const existingDecrypted = await decryptProviderConfig( existing.config, userId, ); @@ -342,7 +353,7 @@ export function registerSSOProviderRoutes(router: Router): void { ), ...rawConfig, }; - encryptedConfig = encryptProviderConfig( + encryptedConfig = await encryptProviderConfig( mergedConfig, userId, String(providerId), @@ -369,7 +380,7 @@ export function registerSSOProviderRoutes(router: Router): void { }); res.json({ ...updated, - config: decryptProviderConfig(updated.config, userId), + config: await decryptProviderConfig(updated.config, userId), }); } catch (err) { authLogger.error("Failed to update SSO provider", err); diff --git a/src/backend/database/routes/sync-references.ts b/src/backend/database/routes/sync-references.ts new file mode 100644 index 0000000..1ed573c --- /dev/null +++ b/src/backend/database/routes/sync-references.ts @@ -0,0 +1,95 @@ +import type { SyncEntityType } from "../repositories/sync-tombstone-repository.js"; + +export type SyncReferenceEntity = "sshCredentials" | "vaultProfiles"; + +interface SyncReference { + field: string; + syncField: string; + entityType: SyncReferenceEntity; +} + +const HOST_REFERENCES: SyncReference[] = [ + { + field: "credentialId", + syncField: "credentialSyncId", + entityType: "sshCredentials", + }, + { + field: "rdpCredentialId", + syncField: "rdpCredentialSyncId", + entityType: "sshCredentials", + }, + { + field: "vncCredentialId", + syncField: "vncCredentialSyncId", + entityType: "sshCredentials", + }, + { + field: "telnetCredentialId", + syncField: "telnetCredentialSyncId", + entityType: "sshCredentials", + }, + { + field: "vaultProfileId", + syncField: "vaultProfileSyncId", + entityType: "vaultProfiles", + }, +]; + +const REFERENCES: Partial> = { + hosts: HOST_REFERENCES, + sshFolders: [HOST_REFERENCES[0]], +}; + +export async function serializeSyncReferences( + entityType: SyncEntityType, + row: Record, + resolveSyncId: ( + entityType: SyncReferenceEntity, + id: number, + ) => Promise, +): Promise> { + const serialized = { ...row }; + for (const reference of REFERENCES[entityType] ?? []) { + const id = serialized[reference.field]; + serialized[reference.syncField] = + typeof id === "number" + ? await resolveSyncId(reference.entityType, id) + : null; + delete serialized[reference.field]; + } + return serialized; +} + +export async function deserializeSyncReferences( + entityType: SyncEntityType, + row: Record, + resolveId: ( + entityType: SyncReferenceEntity, + syncId: string, + ) => Promise, +): Promise> { + const deserialized = { ...row }; + for (const reference of REFERENCES[entityType] ?? []) { + const syncId = deserialized[reference.syncField]; + delete deserialized[reference.syncField]; + delete deserialized[reference.field]; + + if (syncId == null) { + deserialized[reference.field] = null; + continue; + } + if (typeof syncId !== "string") { + throw new Error(`Invalid ${reference.syncField}`); + } + + const id = await resolveId(reference.entityType, syncId); + if (id === null) { + throw new Error( + `Missing ${reference.entityType} dependency ${reference.syncField}=${syncId}`, + ); + } + deserialized[reference.field] = id; + } + return deserialized; +} diff --git a/src/backend/database/routes/sync.ts b/src/backend/database/routes/sync.ts new file mode 100644 index 0000000..4796e8c --- /dev/null +++ b/src/backend/database/routes/sync.ts @@ -0,0 +1,518 @@ +import express, { type Request, type Response } from "express"; +import { and, eq, type SQL } from "drizzle-orm"; +import { + hosts, + sshCredentials, + sshFolders, + snippets, + snippetFolders, + vaultProfiles, + dashboardServiceLinks, + homepageItems, + userPreferences, +} from "../db/schema.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { DataCrypto } from "../../utils/data-crypto.js"; +import { databaseLogger } from "../../utils/logger.js"; +import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import { + createCurrentRepositoryContext, + createCurrentSyncTombstoneRepository, +} from "../repositories/factory.js"; +import type { SyncEntityType } from "../repositories/sync-tombstone-repository.js"; +import { + deserializeSyncReferences, + serializeSyncReferences, + type SyncReferenceEntity, +} from "./sync-references.js"; +import { timestampAtOrAfter } from "../sync-timestamp.js"; + +const router = express.Router(); +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); + +// Encrypted tables need DataCrypto to translate between the wire payload +// (plaintext) and the stored row (encrypted). Everything else is stored +// and synced as-is. +const ENCRYPTED_ENTITY_TABLES: Partial> = { + hosts: "ssh_data", + sshCredentials: "ssh_credentials", +}; + +interface EntityConfig { + table: + | typeof hosts + | typeof sshCredentials + | typeof sshFolders + | typeof snippets + | typeof snippetFolders + | typeof vaultProfiles + | typeof dashboardServiceLinks + | typeof homepageItems + | typeof userPreferences; + // Fields that only make sense on the device that created the row, or + // that are managed elsewhere and must never be overwritten by a sync + // payload from the other side. + readOnlyFields: string[]; + singleton?: boolean; +} + +const ENTITY_CONFIG: Record = { + hosts: { + table: hosts, + readOnlyFields: ["connectionOrigin"], + }, + sshCredentials: { table: sshCredentials, readOnlyFields: [] }, + sshFolders: { table: sshFolders, readOnlyFields: [] }, + snippets: { table: snippets, readOnlyFields: [] }, + snippetFolders: { table: snippetFolders, readOnlyFields: [] }, + vaultProfiles: { table: vaultProfiles, readOnlyFields: [] }, + dashboardServiceLinks: { table: dashboardServiceLinks, readOnlyFields: [] }, + homepageItems: { table: homepageItems, readOnlyFields: [] }, + userPreferences: { + table: userPreferences, + readOnlyFields: ["storageMode"], + singleton: true, + }, +}; + +const VALID_ENTITY_TYPES = new Set(Object.keys(ENTITY_CONFIG)); +type RepositoryContext = ReturnType; + +export function isValidEntityType(value: unknown): value is SyncEntityType { + return typeof value === "string" && VALID_ENTITY_TYPES.has(value); +} + +/** + * Locates the stored row a sync payload corresponds to. + * + * Read and write have to agree on this. A singleton entity is keyed on its + * owner rather than a sync id, and `user_preferences` โ€” the only singleton โ€” + * has no `id` column at all, so an update cannot fall back to one: `table.id` + * is undefined there and drizzle emits `WHERE = ?`. + */ +export function locateSyncRow( + entityType: SyncEntityType, + userId: string, + syncId: string, +): SQL { + const { table, singleton } = ENTITY_CONFIG[entityType]; + + if (singleton) { + return eq(table.userId, userId); + } + + return and( + eq((table as typeof hosts).syncId, syncId), + eq(table.userId, userId), + )!; +} + +async function findReferenceSyncId( + context: RepositoryContext, + entityType: SyncReferenceEntity, + id: number, + userId: string, +): Promise { + if (entityType === "sshCredentials") { + const [row] = await context.drizzle + .select({ syncId: sshCredentials.syncId }) + .from(sshCredentials) + .where(and(eq(sshCredentials.id, id), eq(sshCredentials.userId, userId))) + .limit(1); + return row?.syncId ?? null; + } + + const [row] = await context.drizzle + .select({ syncId: vaultProfiles.syncId }) + .from(vaultProfiles) + .where(and(eq(vaultProfiles.id, id), eq(vaultProfiles.userId, userId))) + .limit(1); + return row?.syncId ?? null; +} + +async function findReferenceId( + context: RepositoryContext, + entityType: SyncReferenceEntity, + syncId: string, + userId: string, +): Promise { + if (entityType === "sshCredentials") { + const [row] = await context.drizzle + .select({ id: sshCredentials.id }) + .from(sshCredentials) + .where( + and( + eq(sshCredentials.syncId, syncId), + eq(sshCredentials.userId, userId), + ), + ) + .limit(1); + return row?.id ?? null; + } + + const [row] = await context.drizzle + .select({ id: vaultProfiles.id }) + .from(vaultProfiles) + .where( + and(eq(vaultProfiles.syncId, syncId), eq(vaultProfiles.userId, userId)), + ) + .limit(1); + return row?.id ?? null; +} + +function requireUserDataKey(userId: string): Buffer { + return DataCrypto.validateUserAccess(userId); +} + +function decryptIfNeeded( + entityType: SyncEntityType, + row: Record, + userId: string, +): Record { + const tableName = ENCRYPTED_ENTITY_TABLES[entityType]; + if (!tableName) return row; + const userDataKey = DataCrypto.getUserDataKey(userId); + if (!userDataKey) return row; + return DataCrypto.decryptRecord( + tableName, + row, + userId, + userDataKey, + ) as Record; +} + +function encryptIfNeeded( + entityType: SyncEntityType, + row: Record, + userId: string, +): Record { + const tableName = ENCRYPTED_ENTITY_TABLES[entityType]; + if (!tableName) return row; + const userDataKey = requireUserDataKey(userId); + return DataCrypto.encryptRecord( + tableName, + row, + userId, + userDataKey, + ) as Record; +} + +export function stripWritePayload( + entityType: SyncEntityType, + payload: Record, +): Record { + const { readOnlyFields } = ENTITY_CONFIG[entityType]; + const clean = { ...payload }; + delete clean.id; + delete clean.userId; + delete clean.syncId; + for (const field of readOnlyFields) delete clean[field]; + return clean; +} + +/** + * @openapi + * /sync/{entityType}: + * get: + * summary: Pull synced rows for an entity type + * description: Returns rows owned by the authenticated user whose updatedAt is newer than `since` (or all rows if omitted). Used by the desktop app's remote sync engine to reconcile the embedded backend against a connected remote server. + * tags: + * - Sync + * parameters: + * - in: path + * name: entityType + * required: true + * schema: + * type: string + * - in: query + * name: since + * schema: + * type: string + * responses: + * 200: + * description: Rows updated since the given timestamp. + * 400: + * description: Unknown entity type. + * 500: + * description: Failed to fetch rows. + */ +router.get( + "/:entityType", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const entityType = req.params.entityType; + if (!isValidEntityType(entityType)) { + return res.status(400).json({ error: "Unknown entity type" }); + } + const since = + typeof req.query.since === "string" && req.query.since + ? req.query.since + : null; + + try { + const { table, singleton } = ENTITY_CONFIG[entityType]; + const context = createCurrentRepositoryContext(); + const conditions = [eq(table.userId, userId)]; + if (since && "updatedAt" in table) { + conditions.push( + timestampAtOrAfter((table as typeof hosts).updatedAt, since), + ); + } + + const rows = await context.drizzle + .select() + .from(table as typeof hosts) + .where(and(...conditions)); + + const decrypted = await Promise.all( + rows.map(async (row) => { + const result = await serializeSyncReferences( + entityType, + decryptIfNeeded(entityType, row as Record, userId), + (referenceType, id) => + findReferenceSyncId(context, referenceType, id, userId), + ); + return singleton + ? { ...result, syncId: `${entityType}:singleton` } + : result; + }), + ); + + res.json({ rows: decrypted }); + } catch (err) { + databaseLogger.error(`Failed to pull sync rows for ${entityType}`, err, { + operation: "sync_pull", + entityType, + userId, + }); + res.status(500).json({ error: "Failed to fetch rows" }); + } + }, +); + +/** + * @openapi + * /sync/tombstones: + * post: + * summary: Report a deletion from the other side of a sync pair + * description: Applies a remote deletion locally (if the row still exists) and records the tombstone so future pulls stay consistent. + * tags: + * - Sync + * responses: + * 200: + * description: Deletion applied (or row already absent). + * 400: + * description: Unknown entity type or missing syncId. + * 500: + * description: Failed to apply deletion. + */ +router.post( + "/tombstones", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const entityType = req.body?.entityType; + const syncId = req.body?.syncId; + if ( + !isValidEntityType(entityType) || + typeof syncId !== "string" || + !syncId + ) { + return res.status(400).json({ error: "Missing entityType or syncId" }); + } + + try { + const { table } = ENTITY_CONFIG[entityType]; + const context = createCurrentRepositoryContext(); + + await context.drizzle + .delete(table as typeof hosts) + .where(locateSyncRow(entityType, userId, syncId)); + + await createCurrentSyncTombstoneRepository().record( + userId, + entityType, + syncId, + ); + await DatabaseSaveTrigger.forceSave("sync_tombstone_applied"); + + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to apply sync tombstone", err, { + operation: "sync_tombstone_apply", + entityType, + userId, + }); + res.status(500).json({ error: "Failed to apply deletion" }); + } + }, +); + +/** + * @openapi + * /sync/{entityType}: + * post: + * summary: Upsert a synced row by syncId + * description: Creates or updates a row by its syncId. Used by the desktop app's remote sync engine to push local-only or newer rows to the other side of a sync pair. + * tags: + * - Sync + * parameters: + * - in: path + * name: entityType + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Row upserted. + * 400: + * description: Unknown entity type or missing syncId. + * 500: + * description: Failed to upsert row. + */ +router.post( + "/:entityType", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const entityType = req.params.entityType; + if (!isValidEntityType(entityType)) { + return res.status(400).json({ error: "Unknown entity type" }); + } + const payload = req.body?.row; + const syncId = payload?.syncId; + if (!payload || typeof syncId !== "string" || !syncId) { + return res.status(400).json({ error: "Missing row.syncId" }); + } + + try { + // singleton is still needed below: those tables have no sync_id column + // for the insert to populate. + const { table, singleton } = ENTITY_CONFIG[entityType]; + const context = createCurrentRepositoryContext(); + + const locateRow = locateSyncRow(entityType, userId, syncId); + + const existingRows = await context.drizzle + .select() + .from(table as typeof hosts) + .where(locateRow) + .limit(1); + const existing = existingRows[0] as Record | undefined; + + const resolvedPayload = await deserializeSyncReferences( + entityType, + payload, + (referenceType, referenceSyncId) => + findReferenceId(context, referenceType, referenceSyncId, userId), + ); + const writePayload = stripWritePayload(entityType, resolvedPayload); + const encryptedPayload = encryptIfNeeded( + entityType, + writePayload, + userId, + ); + + let resultRow: Record; + if (existing) { + const updatedRows = await context.drizzle + .update(table as typeof hosts) + .set(encryptedPayload) + .where(locateRow) + .returning(); + resultRow = updatedRows[0] as Record; + } else { + const insertedRows = await context.drizzle + .insert(table as typeof hosts) + .values( + (singleton + ? { ...encryptedPayload, userId } + : { + ...encryptedPayload, + userId, + syncId, + }) as typeof hosts.$inferInsert, + ) + .returning(); + resultRow = insertedRows[0] as Record; + } + + await DatabaseSaveTrigger.forceSave("sync_upsert"); + + res.json({ + row: decryptIfNeeded(entityType, resultRow, userId), + created: !existing, + }); + } catch (err) { + databaseLogger.error(`Failed to upsert sync row for ${entityType}`, err, { + operation: "sync_upsert", + entityType, + userId, + }); + res.status(500).json({ error: "Failed to upsert row" }); + } + }, +); + +/** + * @openapi + * /sync/{entityType}/tombstones: + * get: + * summary: Pull deletion tombstones for an entity type + * description: Returns tombstones recorded since `since` so the other side of a sync pair can apply the same deletions. + * tags: + * - Sync + * parameters: + * - in: path + * name: entityType + * required: true + * schema: + * type: string + * - in: query + * name: since + * schema: + * type: string + * responses: + * 200: + * description: Tombstones recorded since the given timestamp. + * 400: + * description: Unknown entity type. + * 500: + * description: Failed to fetch tombstones. + */ +router.get( + "/:entityType/tombstones", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const entityType = req.params.entityType; + if (!isValidEntityType(entityType)) { + return res.status(400).json({ error: "Unknown entity type" }); + } + const since = + typeof req.query.since === "string" && req.query.since + ? req.query.since + : null; + + try { + const tombstones = await createCurrentSyncTombstoneRepository().listSince( + userId, + entityType, + since, + ); + res.json({ tombstones }); + } catch (err) { + databaseLogger.error( + `Failed to fetch sync tombstones for ${entityType}`, + err, + { operation: "sync_tombstones_pull", entityType, userId }, + ); + res.status(500).json({ error: "Failed to fetch tombstones" }); + } + }, +); + +export default router; diff --git a/src/backend/database/routes/tailscale-routes.ts b/src/backend/database/routes/tailscale-routes.ts index 924fc2b..a2e3aed 100644 --- a/src/backend/database/routes/tailscale-routes.ts +++ b/src/backend/database/routes/tailscale-routes.ts @@ -1,7 +1,10 @@ -import { Router } from "express"; -import type { RequestHandler, Router as ExpressRouter } from "express"; +import { + Router, + type RequestHandler, + type Router as ExpressRouter, +} from "express"; import { apiLogger } from "../../utils/logger.js"; -import { getProxyAgent } from "../../utils/proxy-agent.js"; +import { getFetchDispatcher } from "../../utils/proxy-agent.js"; import { createCurrentSettingsRepository } from "../repositories/factory.js"; interface TailscaleDevice { @@ -23,10 +26,15 @@ interface TailscaleAPIDevice { nodeId?: string; } -const TAILSCALE_API_BASE = "https://api.tailscale.com/api/v2"; +const DEFAULT_TAILSCALE_API_BASE = "https://api.tailscale.com/api/v2"; const router = Router(); +function normalizeApiBase(raw: string | null): string { + const trimmed = (raw ?? "").trim().replace(/\/+$/, ""); + return trimmed || DEFAULT_TAILSCALE_API_BASE; +} + export function registerTailscaleRoutes( app: ExpressRouter, authenticateJWT: RequestHandler, @@ -56,20 +64,22 @@ export function registerTailscaleRoutes( */ router.get("/devices", authenticateJWT, async (_req, res) => { try { - const apiKey = - (await createCurrentSettingsRepository().get("tailscale_api_key")) ?? - ""; + const settingsRepo = createCurrentSettingsRepository(); + const apiKey = (await settingsRepo.get("tailscale_api_key")) ?? ""; if (!apiKey) { return res.json({ devices: [], hasApiKey: false }); } + const apiBase = normalizeApiBase( + await settingsRepo.get("tailscale_api_base_url"), + ); - const url = `${TAILSCALE_API_BASE}/tailnet/-/devices?fields=all`; + const url = `${apiBase}/tailnet/-/devices?fields=all`; const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": "Termix/1.0", }, - dispatcher: getProxyAgent(url), + dispatcher: getFetchDispatcher(url), }); if (!response.ok) { @@ -78,13 +88,17 @@ export function registerTailscaleRoutes( status: response.status, }); if (response.status === 401 || response.status === 403) { - return res - .status(401) - .json({ error: "Invalid Tailscale API key", devices: [] }); + return res.status(401).json({ + error: "Invalid Tailscale API key", + devices: [], + hasApiKey: true, + }); } - return res - .status(502) - .json({ error: "Tailscale API error", devices: [] }); + return res.status(502).json({ + error: "Tailscale API error", + devices: [], + hasApiKey: true, + }); } const data = (await response.json()) as { devices: TailscaleAPIDevice[] }; @@ -103,9 +117,11 @@ export function registerTailscaleRoutes( apiLogger.error("Failed to fetch Tailscale devices", err, { operation: "tailscale_devices", }); - res - .status(500) - .json({ error: "Failed to fetch Tailscale devices", devices: [] }); + res.status(500).json({ + error: "Failed to fetch Tailscale devices", + devices: [], + hasApiKey: true, + }); } }); diff --git a/src/backend/database/routes/terminal-image-storage-settings.ts b/src/backend/database/routes/terminal-image-storage-settings.ts new file mode 100644 index 0000000..a6efc6a --- /dev/null +++ b/src/backend/database/routes/terminal-image-storage-settings.ts @@ -0,0 +1,264 @@ +import path from "path"; +import { databaseLogger } from "../../utils/logger.js"; + +/** + * Terminal image storage modes. + * + * - `local`: always write to the backend's mapped local storage. Deterministic: + * never falls back to the remote SFTP path. + * - `remote-sftp`: always write to the connected terminal's SSH host over SFTP. + * Deterministic: never falls back to local storage. + * - `auto`: pick by capability only โ€” remote SFTP when a connected terminal + * session exists, local storage otherwise. Configuration never influences + * this choice. + */ +export const TERMINAL_IMAGE_STORAGE_MODES = [ + "auto", + "local", + "remote-sftp", +] as const; + +export type TerminalImageStorageMode = + (typeof TERMINAL_IMAGE_STORAGE_MODES)[number]; + +export interface TerminalImageStorageSettings { + mode: TerminalImageStorageMode; + /** Absolute path on the Termix backend where local-mode files are written. */ + localDir: string; + /** + * Absolute path handed to the terminal agent in local mode. This is the + * host-side view of `localDir` (e.g. /tmp mapped into the container); it is + * the only path ever exposed to callers. + */ + hostPath: string; + ttlMs: number; + maxCount: number; + maxBytes: number; + /** Both localDir and hostPath were explicitly configured and must be probed. */ + localMappingConfigured: boolean; +} + +/** Settings-table keys. Persisted values always win over environment. */ +export const TERMINAL_IMAGE_STORAGE_KEYS = { + mode: "terminal_image_storage_mode", + localDir: "terminal_image_local_dir", + hostPath: "terminal_image_host_path", + ttlMs: "terminal_image_ttl_ms", + maxCount: "terminal_image_max_count", + maxBytes: "terminal_image_max_storage_bytes", +} as const; + +/** + * Legacy environment variables from the original env-only configuration. They + * seed defaults only when no database value exists for the same field. + */ +export const TERMINAL_IMAGE_STORAGE_ENV = { + mode: "TERMIX_IMAGE_STORAGE_MODE", + localDir: "TERMIX_IMAGE_DIR", + hostPath: "TERMIX_IMAGE_HOST_PATH", + ttlMs: "TERMIX_IMAGE_TTL_MS", + maxCount: "TERMIX_MAX_IMAGE_COUNT", + maxBytes: "TERMIX_MAX_IMAGE_STORAGE_BYTES", +} as const; + +export const DEFAULT_IMAGE_TTL_MS = 3_600_000; +export const DEFAULT_IMAGE_MAX_COUNT = 100; +export const DEFAULT_IMAGE_MAX_BYTES = 5_368_709_120; +export const DEFAULT_IMAGE_HOST_PATH = "/tmp/termix-image-v0"; +export const MIN_IMAGE_MAX_BYTES = 1_048_576; + +export function defaultImageLocalDir(env: NodeJS.ProcessEnv): string { + return path.resolve( + path.join(env.DATA_DIR || "./db/data", "termix-image-v0"), + ); +} + +export function parseTerminalImageStorageMode( + value: unknown, +): TerminalImageStorageMode | null { + if (typeof value !== "string") return null; + const normalized = value.trim().toLowerCase(); + return (TERMINAL_IMAGE_STORAGE_MODES as readonly string[]).includes( + normalized, + ) + ? (normalized as TerminalImageStorageMode) + : null; +} + +/** + * Local write directory must be an absolute path without NUL bytes. Relative + * values are rejected rather than resolved: a relative entry silently depends + * on the process cwd, which differs between Docker, systemd and dev runs. + */ +export function parseImageLocalDir(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + if (!trimmed || hasUnsafePathSyntax(trimmed)) return null; + if (!path.isAbsolute(trimmed)) return null; + return path.resolve(trimmed); +} + +/** Agent-visible path. Always POSIX-style and absolute. */ +export function parseImageHostPath(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + if (!trimmed || hasUnsafePathSyntax(trimmed)) return null; + if (!path.posix.isAbsolute(trimmed)) return null; + return path.posix.normalize(trimmed); +} + +/** + * Numeric fields: unparseable values are rejected (caller falls through to the + * next source); parseable but out-of-range values are clamped, matching the + * original env-only behavior. + */ +function parseClampedInt(value: unknown, min: number): number | null { + if (typeof value !== "string" && typeof value !== "number") return null; + const parsed = + typeof value === "number" ? value : Number.parseInt(value.trim(), 10); + if (!Number.isFinite(parsed)) return null; + return Math.max(min, Math.trunc(parsed)); +} + +interface SettingSource { + get(key: string): Promise; +} + +function warnInvalid(key: string, source: string): void { + databaseLogger.warn("Ignoring invalid terminal image storage setting", { + operation: "terminal_image_storage_settings_invalid", + key, + source, + }); +} + +function hasUnsafePathSyntax(value: string): boolean { + return ( + /[\u0000-\u001f\u007f]/.test(value) || + value.split(/[\\/]+/).some((segment) => segment === "..") || + value.includes("//") || + value.includes("\\") + ); +} + +/** + * Resolves the effective image storage settings. + * + * Per-field precedence: a valid persisted database value wins; a legacy + * TERMIX_IMAGE_* variable seeds the default only when no database value + * exists; otherwise the built-in default applies. Invalid values are skipped + * with a warning and resolution falls through to the next source. + * + * Mode compatibility: with no mode in the database or environment, an explicit + * legacy local mapping (TERMIX_IMAGE_DIR) keeps old deployments on `local`; + * everything else defaults to `auto`. + */ +export async function resolveTerminalImageStorageSettings( + settings: SettingSource, + env: NodeJS.ProcessEnv = process.env, +): Promise { + async function pick( + key: string, + envName: string, + parse: (value: unknown) => T | null, + fallback: T, + ): Promise { + const stored = await settings.get(key); + if (stored !== null) { + const parsed = parse(stored); + if (parsed !== null) return parsed; + warnInvalid(key, "database"); + } + const fromEnv = env[envName]; + if (fromEnv !== undefined) { + const parsed = parse(fromEnv); + if (parsed !== null) return parsed; + warnInvalid(key, "environment"); + } + return fallback; + } + + const dbModeRaw = await settings.get(TERMINAL_IMAGE_STORAGE_KEYS.mode); + let mode: TerminalImageStorageMode | null = null; + if (dbModeRaw !== null) { + mode = parseTerminalImageStorageMode(dbModeRaw); + if (mode === null) + warnInvalid(TERMINAL_IMAGE_STORAGE_KEYS.mode, "database"); + } + if (mode === null) { + const envModeRaw = env[TERMINAL_IMAGE_STORAGE_ENV.mode]; + if (envModeRaw !== undefined) { + mode = parseTerminalImageStorageMode(envModeRaw); + if (mode === null) + warnInvalid(TERMINAL_IMAGE_STORAGE_KEYS.mode, "environment"); + } + } + if (mode === null) { + // Legacy deployments configured local storage purely through + // TERMIX_IMAGE_DIR; keep them on local mode unless a database value says + // otherwise. + mode = + env[TERMINAL_IMAGE_STORAGE_ENV.localDir] !== undefined ? "local" : "auto"; + } + + const legacyLocalDir = parseImageLocalDir( + env[TERMINAL_IMAGE_STORAGE_ENV.localDir], + ); + const [localDir, hostPath, ttlMs, maxCount, maxBytes] = await Promise.all([ + pick( + TERMINAL_IMAGE_STORAGE_KEYS.localDir, + TERMINAL_IMAGE_STORAGE_ENV.localDir, + parseImageLocalDir, + defaultImageLocalDir(env), + ), + pick( + TERMINAL_IMAGE_STORAGE_KEYS.hostPath, + TERMINAL_IMAGE_STORAGE_ENV.hostPath, + parseImageHostPath, + legacyLocalDir ?? DEFAULT_IMAGE_HOST_PATH, + ), + pick( + TERMINAL_IMAGE_STORAGE_KEYS.ttlMs, + TERMINAL_IMAGE_STORAGE_ENV.ttlMs, + (value) => parseClampedInt(value, 0), + DEFAULT_IMAGE_TTL_MS, + ), + pick( + TERMINAL_IMAGE_STORAGE_KEYS.maxCount, + TERMINAL_IMAGE_STORAGE_ENV.maxCount, + (value) => parseClampedInt(value, 1), + DEFAULT_IMAGE_MAX_COUNT, + ), + pick( + TERMINAL_IMAGE_STORAGE_KEYS.maxBytes, + TERMINAL_IMAGE_STORAGE_ENV.maxBytes, + (value) => parseClampedInt(value, MIN_IMAGE_MAX_BYTES), + DEFAULT_IMAGE_MAX_BYTES, + ), + ]); + + const persistedLocalDir = await settings.get( + TERMINAL_IMAGE_STORAGE_KEYS.localDir, + ); + const persistedHostPath = await settings.get( + TERMINAL_IMAGE_STORAGE_KEYS.hostPath, + ); + const localMappingConfigured = + ((persistedLocalDir !== null && + parseImageLocalDir(persistedLocalDir) !== null) || + parseImageLocalDir(env[TERMINAL_IMAGE_STORAGE_ENV.localDir]) !== null) && + ((persistedHostPath !== null && + parseImageHostPath(persistedHostPath) !== null) || + parseImageHostPath(env[TERMINAL_IMAGE_STORAGE_ENV.hostPath]) !== null || + legacyLocalDir !== null); + + return { + mode, + localDir, + hostPath, + ttlMs, + maxCount, + maxBytes, + localMappingConfigured, + }; +} diff --git a/src/backend/database/routes/terminal-image-storage.ts b/src/backend/database/routes/terminal-image-storage.ts new file mode 100644 index 0000000..7022a9e --- /dev/null +++ b/src/backend/database/routes/terminal-image-storage.ts @@ -0,0 +1,675 @@ +import fs from "fs/promises"; +import path from "path"; +import { randomUUID } from "crypto"; +import { + exceedsImageStorageLimit, + isExpiredImage, + isImageFilename, +} from "./terminal-image-utils.js"; +import type { TerminalImageStorageSettings } from "./terminal-image-storage-settings.js"; + +/** + * Stable error codes for image storage failures. These are part of the upload + * route's contract; `IMAGE_REMOTE_WRITE_FAILED` predates this module and must + * not change. + */ +export type TerminalImageStorageErrorCode = + | "IMAGE_STORAGE_LIMIT_REACHED" + | "IMAGE_LOCAL_WRITE_FAILED" + | "IMAGE_LOCAL_INSPECTION_FAILED" + | "IMAGE_REMOTE_QUOTA_UNAVAILABLE" + | "IMAGE_REMOTE_WRITE_FAILED"; + +export class TerminalImageStorageError extends Error { + constructor( + readonly code: TerminalImageStorageErrorCode, + message: string, + readonly cause?: unknown, + ) { + super(message); + this.name = "TerminalImageStorageError"; + } +} + +export interface StoredTerminalImage { + id: string; + filename: string; + /** Agent-visible path; never a backend-internal path. */ + shellPath: string; + storage: "local" | "remote-sftp"; +} + +/** + * Mode selection for one upload. Explicit modes are deterministic โ€” they are + * returned regardless of capability and the route reports the failure; only + * `auto` falls back, and only on capability (a connected terminal session + * with SFTP), never on configuration. + */ +export function selectImageStorageMode( + settings: Pick< + TerminalImageStorageSettings, + "mode" | "localMappingConfigured" + >, + capability: { remoteSftpAvailable: boolean; localHostVisible?: boolean }, +): "local" | "remote-sftp" | "unavailable" { + if (settings.mode === "local") return "local"; + if (settings.mode === "remote-sftp") return "remote-sftp"; + if (settings.localMappingConfigured && capability.localHostVisible === true) { + return "local"; + } + return capability.remoteSftpAvailable ? "remote-sftp" : "unavailable"; +} + +// Capacity checks and writes are serialized so concurrent uploads cannot +// bypass the count or byte limits. +let imageStorageQueue: Promise = Promise.resolve(); + +function withImageStorageLock(operation: () => Promise): Promise { + const result = imageStorageQueue.then(operation, operation); + imageStorageQueue = result.then( + () => undefined, + () => undefined, + ); + return result; +} + +async function cleanupExpiredImages( + localDir: string, + ttlMs: number, +): Promise { + const entries = await fs.readdir(localDir, { withFileTypes: true }); + const now = Date.now(); + await Promise.all( + entries + .filter((entry) => entry.isFile() && isImageFilename(entry.name)) + .map(async (entry) => { + const filePath = path.join(localDir, entry.name); + const stat = await fs.stat(filePath); + if (stat && isExpiredImage(stat.mtimeMs, now, ttlMs)) { + await fs.unlink(filePath).catch(() => undefined); + } + }), + ); +} + +async function getActiveImageStorageUsage( + localDir: string, + ttlMs: number, +): Promise<{ fileCount: number; totalBytes: number }> { + const entries = await fs.readdir(localDir, { withFileTypes: true }); + const now = Date.now(); + const stats = await Promise.all( + entries + .filter((entry) => entry.isFile() && isImageFilename(entry.name)) + .map(async (entry) => { + const stat = await fs.stat(path.join(localDir, entry.name)); + return !isExpiredImage(stat.mtimeMs, now, ttlMs) ? stat : null; + }), + ); + + return stats.reduce( + (usage, stat) => { + if (stat) { + usage.fileCount += 1; + usage.totalBytes += stat.size; + } + return usage; + }, + { fileCount: 0, totalBytes: 0 }, + ); +} + +/** + * Local mapped-storage adapter. Enforces the TTL/count/byte policy and raises + * `IMAGE_STORAGE_LIMIT_REACHED` (HTTP 507 at the route) when the caps are hit. + * The returned shellPath is built from the agent-visible hostPath โ€” the + * backend's own localDir is never exposed. + */ +export async function storeImageLocally( + image: Buffer, + settings: TerminalImageStorageSettings, +): Promise { + return withImageStorageLock(async () => { + let usage: { fileCount: number; totalBytes: number }; + try { + await fs.mkdir(settings.localDir, { recursive: true }); + await cleanupExpiredImages(settings.localDir, settings.ttlMs); + usage = await getActiveImageStorageUsage( + settings.localDir, + settings.ttlMs, + ); + } catch (error) { + throw new TerminalImageStorageError( + "IMAGE_LOCAL_INSPECTION_FAILED", + "Unable to inspect local image storage", + error, + ); + } + if ( + exceedsImageStorageLimit( + usage.fileCount, + usage.totalBytes, + image.length, + settings.maxCount, + settings.maxBytes, + ) + ) { + throw new TerminalImageStorageError( + "IMAGE_STORAGE_LIMIT_REACHED", + "Image storage limit reached", + ); + } + + const id = randomUUID(); + const filename = `${id}.png`; + try { + await fs.writeFile(path.join(settings.localDir, filename), image); + } catch (error) { + await fs + .rm(path.join(settings.localDir, filename), { force: true }) + .catch(() => undefined); + throw new TerminalImageStorageError( + "IMAGE_LOCAL_WRITE_FAILED", + "Failed to write image to local storage", + error, + ); + } + + return { + id, + filename, + shellPath: path.posix.join(settings.hostPath, filename), + storage: "local", + }; + }); +} + +// Remote directory (on the SSH host the terminal is connected to) that +// uploaded/pasted images are written into. Always POSIX-style: this is a +// path on the remote shell, not on the Termix backend's own filesystem. +export const REMOTE_IMAGE_DIR = "/tmp/termix-images"; + +/** Minimal SFTP surface the remote adapter needs (satisfied by ssh2). */ +export interface ImageSftpClient { + mkdir( + dir: string, + attrsOrCallback: { mode?: number } | ((err?: Error) => void), + callback?: (err?: Error) => void, + ): void; + createWriteStream( + remotePath: string, + options?: { mode?: number }, + ): NodeJS.WritableStream; + stat?: ( + dir: string, + callback: ( + error: Error | undefined, + attrs?: { mode?: number; mtime?: number }, + ) => void, + ) => void; + lstat?: ( + dir: string, + callback: ( + error: Error | undefined, + attrs?: { mode?: number; mtime?: number }, + ) => void, + ) => void; + chmod?: ( + dir: string, + mode: number, + callback: (error?: Error) => void, + ) => void; + readdir?: ( + dir: string, + callback: (error: Error | undefined, entries: ImageSftpEntry[]) => void, + ) => void; + unlink?: (remotePath: string, callback: (error?: Error) => void) => void; + rmdir?: (dir: string, callback: (error?: Error) => void) => void; + end?: () => void; +} + +interface ImageSftpEntry { + filename: string; + attrs?: { mtime?: number; size?: number }; +} + +export interface ImageSshExecClient { + exec( + command: string, + callback: (error: Error | undefined, stream?: ImageExecStream) => void, + ): void; +} + +interface ImageExecStream { + on(event: "close", listener: (code: number | null) => void): this; + on(event: "error", listener: (error: Error) => void): this; + resume(): void; +} + +function quoteRemotePath(value: string): string { + return `'${value.replace(/'/g, `'"'"'`)}'`; +} + +function execBounded( + sshConn: ImageSshExecClient, + command: string, + timeoutMs = 3_000, +): Promise { + return new Promise((resolve) => { + let settled = false; + const finish = (result: boolean) => { + if (settled) return; + settled = true; + resolve(result); + }; + const timer = setTimeout(() => finish(false), timeoutMs); + sshConn.exec(command, (error, stream) => { + if (error || !stream) { + clearTimeout(timer); + finish(false); + return; + } + stream.on("close", (code) => { + clearTimeout(timer); + finish(code === 0); + }); + stream.on("error", () => { + clearTimeout(timer); + finish(false); + }); + stream.resume(); + }); + }); +} + +function withTimeout( + operation: Promise, + timeoutMs: number, + message: string, +): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(message)), timeoutMs); + operation.then( + (value) => { + clearTimeout(timer); + resolve(value); + }, + (error) => { + clearTimeout(timer); + reject(error); + }, + ); + }); +} + +/** Verify a configured local mapping from the currently connected SSH session. */ +export async function probeLocalImageVisibility( + sshConn: ImageSshExecClient, + settings: Pick, +): Promise { + const filename = `.termix-image-probe-${randomUUID()}`; + const localProbe = path.join(settings.localDir, filename); + const remoteProbe = path.posix.join(settings.hostPath, filename); + await fs.mkdir(settings.localDir, { recursive: true }); + await fs.writeFile(localProbe, "termix-image-probe", { flag: "wx" }); + try { + return await execBounded( + sshConn, + `test -f -- ${quoteRemotePath(remoteProbe)}`, + ); + } finally { + await fs.unlink(localProbe).catch(() => undefined); + await execBounded(sshConn, `rm -f -- ${quoteRemotePath(remoteProbe)}`); + } +} + +function sftpMkdir(sftp: ImageSftpClient, dir: string): Promise { + return new Promise((resolve, reject) => { + sftp.mkdir(dir, { mode: 0o700 }, (err) => { + if (!err) { + resolve(); + return; + } + const inspect = (sftp.lstat ?? sftp.stat)?.bind(sftp); + if (!inspect) { + reject(err); + return; + } + inspect(dir, (inspectError, attrs) => { + if (inspectError || !attrs) { + reject(inspectError ?? err); + return; + } + if (attrs.mode !== undefined && (attrs.mode & 0o170000) !== 0o040000) { + reject(new Error("Remote image path is not a directory")); + return; + } + if (!sftp.chmod) { + reject( + new Error("Remote image directory permissions cannot be verified"), + ); + return; + } + sftp.chmod(dir, 0o700, (chmodError) => { + if (chmodError) reject(chmodError); + else resolve(); + }); + }); + }); + }); +} + +const REMOTE_IMAGE_LOCK_DIR = `${REMOTE_IMAGE_DIR}/.termix-write-lock`; +const REMOTE_IMAGE_LOCK_LEASE_MS = 60_000; + +function waitForRemoteImageLock( + sftp: ImageSftpClient, + attempts = 60, +): Promise<() => Promise> { + if (!sftp.rmdir) { + return Promise.reject( + new TerminalImageStorageError( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + "Remote image storage lock cannot be verified", + ), + ); + } + + return new Promise((resolve, reject) => { + let remaining = attempts; + const tryAcquire = () => { + sftp.mkdir(REMOTE_IMAGE_LOCK_DIR, { mode: 0o700 }, (error) => { + if (!error) { + resolve( + () => + new Promise((releaseResolve, releaseReject) => { + sftp.rmdir!(REMOTE_IMAGE_LOCK_DIR, (releaseError) => + releaseError ? releaseReject(releaseError) : releaseResolve(), + ); + }), + ); + return; + } + + const inspect = (sftp.lstat ?? sftp.stat)?.bind(sftp); + if (!inspect) { + reject( + new TerminalImageStorageError( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + "Remote image storage lock cannot be verified", + error, + ), + ); + return; + } + inspect(REMOTE_IMAGE_LOCK_DIR, (inspectError, attrs) => { + const stale = + !inspectError && + typeof attrs?.mtime === "number" && + Date.now() - attrs.mtime * 1000 > REMOTE_IMAGE_LOCK_LEASE_MS; + if (stale) { + sftp.rmdir!(REMOTE_IMAGE_LOCK_DIR, (removeError) => { + if (removeError) { + if (--remaining <= 0) { + reject( + new TerminalImageStorageError( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + "Stale remote image storage lock cannot be removed", + removeError, + ), + ); + return; + } + setTimeout(tryAcquire, 50); + return; + } + tryAcquire(); + }); + return; + } + if (--remaining <= 0) { + reject( + new TerminalImageStorageError( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + "Remote image storage lock is unavailable", + error, + ), + ); + return; + } + setTimeout(tryAcquire, 50); + }); + }); + }; + tryAcquire(); + }); +} + +function sftpWriteFile( + sftp: ImageSftpClient, + remotePath: string, + data: Buffer, + timeoutMs = 10_000, +): Promise { + return new Promise((resolve, reject) => { + const stream = sftp.createWriteStream(remotePath, { + mode: 0o600, + }) as NodeJS.WritableStream & { + destroy?: () => void; + }; + let settled = false; + const timer = setTimeout(() => { + if (settled) return; + settled = true; + stream.destroy?.(); + reject(new Error("SFTP image write timed out")); + }, timeoutMs); + const finish = (error?: Error) => { + if (settled) return; + settled = true; + clearTimeout(timer); + if (error) reject(error); + else resolve(); + }; + stream.on("error", (error: Error) => finish(error)); + stream.on("close", () => finish()); + stream.end(data); + }); +} + +async function cleanupExpiredRemoteImages( + sftp: ImageSftpClient, + ttlMs: number | undefined, + nowMs = Date.now(), +): Promise { + if (!ttlMs || ttlMs <= 0 || !sftp.readdir || !sftp.unlink) return; + const entries = await new Promise((resolve) => { + sftp.readdir!(REMOTE_IMAGE_DIR, (error, result) => { + resolve(error ? [] : result); + }); + }); + const cutoffSeconds = (nowMs - ttlMs) / 1000; + const uuidPng = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.png$/i; + await Promise.all( + entries + .filter( + (entry) => + uuidPng.test(entry.filename) && + typeof entry.attrs?.mtime === "number" && + entry.attrs.mtime < cutoffSeconds, + ) + .map((entry) => + withTimeout( + new Promise((resolve) => { + sftp.unlink!(`${REMOTE_IMAGE_DIR}/${entry.filename}`, () => + resolve(), + ); + }), + 3_000, + "SFTP cleanup operation timed out", + ).catch(() => undefined), + ), + ); +} + +const REMOTE_UUID_PNG_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.png$/i; + +async function enforceRemoteImageLimits( + sftp: ImageSftpClient, + imageBytes: number, + maxCount: number, + maxBytes: number, +): Promise { + if (!sftp.readdir) { + throw new TerminalImageStorageError( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + "Remote image storage limits cannot be verified", + ); + } + let entries: ImageSftpEntry[]; + try { + entries = await new Promise((resolve, reject) => { + sftp.readdir!(REMOTE_IMAGE_DIR, (error, result) => { + if (error) reject(error); + else resolve(result); + }); + }); + } catch (error) { + throw new TerminalImageStorageError( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + "Remote image storage limits cannot be verified", + error, + ); + } + const images = entries.filter((entry) => + REMOTE_UUID_PNG_PATTERN.test(entry.filename), + ); + const totalBytes = images.reduce((sum, entry) => { + if (typeof entry.attrs?.size !== "number") { + throw new TerminalImageStorageError( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + "Remote image storage limits cannot be verified", + ); + } + return sum + entry.attrs.size; + }, 0); + if (images.length >= maxCount || totalBytes + imageBytes > maxBytes) { + throw new TerminalImageStorageError( + "IMAGE_STORAGE_LIMIT_REACHED", + "Image storage limit reached", + ); + } +} +async function storeImageViaSftpUnlocked( + sftp: ImageSftpClient, + image: Buffer, + options: { + writeTimeoutMs?: number; + ttlMs?: number; + maxCount?: number; + maxBytes?: number; + nowMs?: number; + } = {}, +): Promise { + const id = randomUUID(); + const filename = `${id}.png`; + const remotePath = `${REMOTE_IMAGE_DIR}/${filename}`; + + let releaseRemoteLock: (() => Promise) | undefined; + let operationError: TerminalImageStorageError | undefined; + try { + await withTimeout( + sftpMkdir(sftp, REMOTE_IMAGE_DIR), + 3_000, + "SFTP directory operation timed out", + ); + releaseRemoteLock = await withTimeout( + waitForRemoteImageLock(sftp), + 10_000, + "SFTP lock operation timed out", + ); + await withTimeout( + cleanupExpiredRemoteImages(sftp, options.ttlMs, options.nowMs), + 5_000, + "SFTP cleanup operation timed out", + ); + if (options.maxCount !== undefined || options.maxBytes !== undefined) { + await withTimeout( + enforceRemoteImageLimits( + sftp, + image.length, + options.maxCount ?? 100, + options.maxBytes ?? 5_368_709_120, + ), + 5_000, + "SFTP quota operation timed out", + ); + } + await sftpWriteFile(sftp, remotePath, image, options.writeTimeoutMs); + } catch (error) { + if (sftp.unlink) { + await withTimeout( + new Promise((resolve) => { + sftp.unlink!(remotePath, () => resolve()); + }), + 3_000, + "SFTP cleanup operation timed out", + ).catch(() => undefined); + } + operationError = + error instanceof TerminalImageStorageError + ? error + : new TerminalImageStorageError( + "IMAGE_REMOTE_WRITE_FAILED", + "Failed to write image to the remote host", + error, + ); + } + + if (releaseRemoteLock) { + try { + await withTimeout( + releaseRemoteLock(), + 3_000, + "SFTP lock release timed out", + ); + } catch (releaseError) { + if (!operationError) { + if (sftp.unlink) { + await withTimeout( + new Promise((resolve) => { + sftp.unlink!(remotePath, () => resolve()); + }), + 3_000, + "SFTP cleanup operation timed out", + ).catch(() => undefined); + } + operationError = new TerminalImageStorageError( + "IMAGE_REMOTE_WRITE_FAILED", + "Failed to release remote image storage lock", + releaseError, + ); + } + } + } + + if (operationError) throw operationError; + + return { id, filename, shellPath: remotePath, storage: "remote-sftp" }; +} + +export function storeImageViaSftp( + sftp: ImageSftpClient, + image: Buffer, + options: Parameters[2] = {}, +): Promise { + return withImageStorageLock(() => + withTimeout( + storeImageViaSftpUnlocked(sftp, image, options), + 20_000, + "SFTP image operation timed out", + ), + ); +} diff --git a/src/backend/database/routes/terminal-image-utils.ts b/src/backend/database/routes/terminal-image-utils.ts new file mode 100644 index 0000000..f6eb004 --- /dev/null +++ b/src/backend/database/routes/terminal-image-utils.ts @@ -0,0 +1,100 @@ +// Accepted decoded input formats; uploads are normalized to PNG by the route. +export const IMAGE_FORMAT_EXTENSIONS: Record = { + avif: "avif", + gif: "gif", + heif: "heif", + jpeg: "jpg", + jp2: "jp2", + jxl: "jxl", + png: "png", + tiff: "tiff", + webp: "webp", +}; + +export function imageExtensionForFormat( + format: string | undefined, +): string | undefined { + return format ? IMAGE_FORMAT_EXTENSIONS[format] : undefined; +} + +export const MAX_NORMALIZED_IMAGE_BYTES = 10 * 1024 * 1024; + +export function exceedsNormalizedImageSize( + byteLength: number, + maxBytes = MAX_NORMALIZED_IMAGE_BYTES, +): boolean { + return byteLength > maxBytes; +} + +export function createConcurrencyLimiter( + limit: number, + maxQueued = Number.POSITIVE_INFINITY, +): { + acquire: () => Promise<() => void>; + readonly active: number; + readonly queued: number; +} { + const max = Math.max(1, Math.floor(limit)); + const queueLimit = Math.max(0, Math.floor(maxQueued)); + let active = 0; + const waiters: Array<() => void> = []; + + const startNext = () => { + if (active >= max || waiters.length === 0) return; + active += 1; + waiters.shift()!(); + }; + + return { + acquire: () => + new Promise<() => void>((resolve, reject) => { + if (active >= max && waiters.length >= queueLimit) { + reject(new Error("Concurrency admission queue is full")); + return; + } + waiters.push(() => { + let released = false; + resolve(() => { + if (released) return; + released = true; + active -= 1; + startNext(); + }); + }); + startNext(); + }), + get active() { + return active; + }, + get queued() { + return waiters.length; + }, + }; +} + +export const IMAGE_FILENAME_PATTERN = /^[0-9a-f-]{36}\.[a-z0-9]+$/i; + +export function isImageFilename(filename: string): boolean { + return IMAGE_FILENAME_PATTERN.test(filename); +} + +export function isExpiredImage( + modifiedAtMs: number, + nowMs: number, + ttlMs: number, +): boolean { + if (ttlMs <= 0) return false; + return nowMs - modifiedAtMs > ttlMs; +} + +export function exceedsImageStorageLimit( + fileCount: number, + totalBytes: number, + incomingBytes: number, + maxFileCount: number, + maxStorageBytes: number, +): boolean { + return ( + fileCount >= maxFileCount || totalBytes + incomingBytes > maxStorageBytes + ); +} diff --git a/src/backend/database/routes/terminal.ts b/src/backend/database/routes/terminal.ts index cdc3799..81085b2 100644 --- a/src/backend/database/routes/terminal.ts +++ b/src/backend/database/routes/terminal.ts @@ -1,8 +1,30 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { + type NextFunction, + type Request, + type Response, +} from "express"; +import { randomUUID } from "crypto"; +import multer from "multer"; +import sharp from "sharp"; +import { + createConcurrencyLimiter, + exceedsNormalizedImageSize, + imageExtensionForFormat, +} from "./terminal-image-utils.js"; +import { resolveTerminalImageStorageSettings } from "./terminal-image-storage-settings.js"; +import { + selectImageStorageMode, + probeLocalImageVisibility, + storeImageLocally, + storeImageViaSftp, + TerminalImageStorageError, + type ImageSftpClient, +} from "./terminal-image-storage.js"; import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; +import { sessionManager } from "../../hosts/terminal/session-manager.js"; import { createCurrentCommandHistoryRepository, createCurrentHostResolutionRepository, @@ -19,6 +41,293 @@ const authManager = AuthManager.getInstance(); const authenticateJWT = authManager.createAuthMiddleware(); const requireDataAccess = authManager.createDataAccessMiddleware(); +// Browser image handoff for local terminal-agent workflows. +const imageUpload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 50 * 1024 * 1024, + fields: 4, + fieldSize: 64 * 1024, + files: 1, + parts: 5, + headerPairs: 200, + }, +}); +const imageUploadMiddleware = imageUpload.single("image"); +const imageProcessingLimiter = createConcurrencyLimiter(4, 4); +const imageMultipartAdmissionLimiter = createConcurrencyLimiter(4, 4); +let imageUploadSequence = 0; + +async function handleImageUploadMiddleware( + req: Request, + res: Response, + next: NextFunction, +): Promise { + let releaseAdmission: (() => void) | undefined; + try { + releaseAdmission = await imageMultipartAdmissionLimiter.acquire(); + } catch { + res.status(503).json({ + error: "Image upload capacity is temporarily unavailable", + code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED", + }); + return; + } + + imageUploadMiddleware(req, res, (error: unknown) => { + try { + if (!error) { + next(); + return; + } + if (error instanceof multer.MulterError) { + databaseLogger.warn("Image upload multipart request rejected", { + operation: "terminal_image_upload_multipart_rejected", + code: error.code, + field: error.field, + contentType: req.headers["content-type"]?.split(";", 1)[0], + }); + res.status(400).json({ + error: "Image upload request rejected", + code: error.code, + field: error.field, + }); + return; + } + databaseLogger.warn("Image upload multipart request malformed", { + operation: "terminal_image_upload_multipart_invalid", + contentType: req.headers["content-type"]?.split(";", 1)[0], + }); + res.status(400).json({ + error: "Malformed image upload request", + code: "IMAGE_MULTIPART_INVALID", + }); + } finally { + releaseAdmission?.(); + } + }); +} +function findTerminalSession(userId: string, instanceId: string) { + return sessionManager + .getUserSessions(userId) + .find( + (session) => + (session.attachedTabInstanceId ?? session.tabInstanceId) === + instanceId && session.isConnected, + ); +} + +router.post( + "/image-upload", + authenticateJWT, + requireDataAccess, + handleImageUploadMiddleware, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const instanceId = req.body?.instanceId; + if (!req.file) { + return res.status(400).json({ + error: "Image required", + code: "IMAGE_FILE_MISSING", + }); + } + if (!isNonEmptyString(userId)) { + return res.status(400).json({ + error: "Missing terminal session", + code: "IMAGE_SESSION_MISSING", + }); + } + + const requestId = randomUUID(); + const sequence = ++imageUploadSequence; + const source = + req.body?.source === "file" || req.body?.source === "clipboard" + ? req.body.source + : undefined; + const clientUploadTimestamp = + typeof req.body?.clientUploadTimestamp === "string" && + !Number.isNaN(Date.parse(req.body.clientUploadTimestamp)) + ? req.body.clientUploadTimestamp + : undefined; + const serverReceivedAt = new Date().toISOString(); + databaseLogger.info("Terminal image upload received", { + operation: "terminal_image_upload_received", + requestId, + sequence, + source, + clientUploadTimestamp, + serverReceivedAt, + bytes: req.file.size, + }); + + const storageSettings = await resolveTerminalImageStorageSettings( + createCurrentSettingsRepository(), + ); + const session = isNonEmptyString(instanceId) + ? findTerminalSession(userId, instanceId) + : undefined; + let localHostVisible = false; + if (storageSettings.localMappingConfigured && session?.sshConn) { + localHostVisible = await probeLocalImageVisibility( + session.sshConn, + storageSettings, + ).catch(() => false); + } + const storageMode = selectImageStorageMode(storageSettings, { + remoteSftpAvailable: !!session?.sshConn, + localHostVisible, + }); + + if (storageMode === "unavailable") { + return res.status(503).json({ + error: "Image storage is unavailable", + code: "IMAGE_STORAGE_UNAVAILABLE", + }); + } + + if (storageMode === "local" && !storageSettings.localMappingConfigured) { + return res.status(503).json({ + error: "Local image storage is not configured", + code: "IMAGE_LOCAL_STORAGE_NOT_CONFIGURED", + }); + } + + if (storageMode === "remote-sftp") { + if (!isNonEmptyString(instanceId)) { + return res.status(400).json({ + error: "Missing terminal session", + code: "IMAGE_SESSION_MISSING", + }); + } + if (!session || !session.sshConn) { + return res.status(409).json({ + error: "Terminal is not connected", + code: "IMAGE_TERMINAL_NOT_CONNECTED", + }); + } + } + + let normalizedImage: Buffer; + let releaseImageProcessingSlot: (() => void) | undefined; + try { + releaseImageProcessingSlot = await imageProcessingLimiter.acquire(); + } catch { + return res.status(503).json({ + error: "Image upload capacity is temporarily exhausted", + code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED", + }); + } + try { + const source = sharp(req.file.buffer, { + failOn: "error", + limitInputPixels: 40_000_000, + }); + const { format } = await source.metadata(); + if (!imageExtensionForFormat(format)) { + return res.status(400).json({ + error: "Unsupported image format", + code: "IMAGE_FORMAT_UNSUPPORTED", + }); + } + normalizedImage = await source.rotate().png().toBuffer(); + if (exceedsNormalizedImageSize(normalizedImage.length)) { + return res.status(413).json({ + error: "Normalized image is too large", + code: "IMAGE_NORMALIZED_SIZE_LIMIT", + }); + } + } catch (error) { + databaseLogger.warn("Image upload failed image decoding", { + operation: "terminal_image_upload_decode", + mimeType: req.file.mimetype, + bytes: req.file.size, + reason: getErrorMessage(error, "unknown"), + }); + return res.status(400).json({ + error: "Invalid image data", + code: "IMAGE_DECODE_FAILED", + }); + } finally { + releaseImageProcessingSlot?.(); + } + + let remoteSftp: ImageSftpClient | undefined; + try { + const stored = + storageMode === "remote-sftp" + ? await (async () => { + remoteSftp = await new Promise( + (resolve, reject) => { + let settled = false; + const timer = setTimeout(() => { + settled = true; + reject(new Error("SFTP channel acquisition timed out")); + }, 3_000); + session!.sshConn!.sftp((err, sftp) => { + if (settled) { + sftp?.end?.(); + return; + } + settled = true; + clearTimeout(timer); + if (err) return reject(err); + resolve(sftp); + }); + }, + ); + return storeImageViaSftp(remoteSftp, normalizedImage, { + ttlMs: storageSettings.ttlMs, + maxCount: storageSettings.maxCount, + maxBytes: storageSettings.maxBytes, + }); + })() + : await storeImageLocally(normalizedImage, storageSettings); + + res.json(stored); + } catch (error) { + if (error instanceof TerminalImageStorageError) { + const status = + error.code === "IMAGE_STORAGE_LIMIT_REACHED" + ? 507 + : error.code === "IMAGE_REMOTE_WRITE_FAILED" + ? 502 + : error.code === "IMAGE_REMOTE_QUOTA_UNAVAILABLE" + ? 503 + : error.code === "IMAGE_LOCAL_INSPECTION_FAILED" + ? 503 + : 500; + databaseLogger.warn("Image upload storage write failed", { + operation: + error.code === "IMAGE_REMOTE_WRITE_FAILED" + ? "terminal_image_upload_sftp_failed" + : "terminal_image_upload_local_failed", + code: error.code, + userId, + instanceId, + reason: getErrorMessage(error.cause ?? error, "unknown"), + }); + return res.status(status).json({ + error: error.message, + code: error.code, + }); + } + databaseLogger.warn("Image upload failed to acquire remote channel", { + operation: "terminal_image_upload_sftp_failed", + code: "IMAGE_REMOTE_WRITE_FAILED", + userId, + instanceId, + reason: getErrorMessage(error, "unknown"), + }); + return res.status(502).json({ + error: "Failed to write image to the remote host", + code: "IMAGE_REMOTE_WRITE_FAILED", + }); + } finally { + remoteSftp?.end?.(); + } + }, +); + /** * @openapi * /terminal/command_history: @@ -130,7 +439,7 @@ router.post( } catch (err) { authLogger.error("Failed to save command to history", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to save command", + error: getErrorMessage(err, "Failed to save command"), }); } }, @@ -188,7 +497,7 @@ router.get( } catch (err) { authLogger.error("Failed to fetch command history", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to fetch history", + error: getErrorMessage(err, "Failed to fetch history"), }); } }, @@ -252,7 +561,7 @@ router.post( } catch (err) { authLogger.error("Failed to delete command from history", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to delete command", + error: getErrorMessage(err, "Failed to delete command"), }); } }, @@ -311,7 +620,7 @@ router.delete( } catch (err) { authLogger.error("Failed to clear command history", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to clear history", + error: getErrorMessage(err, "Failed to clear history"), }); } }, @@ -352,7 +661,7 @@ router.get( } catch (err) { authLogger.error("Failed to fetch session settings", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to fetch settings", + error: getErrorMessage(err, "Failed to fetch settings"), }); } }, @@ -422,7 +731,7 @@ router.post( } catch (err) { authLogger.error("Failed to save session settings", err); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to save settings", + error: getErrorMessage(err, "Failed to save settings"), }); } }, diff --git a/src/backend/database/routes/termix-id.ts b/src/backend/database/routes/termix-id.ts index ed8e72d..6c9d81b 100644 --- a/src/backend/database/routes/termix-id.ts +++ b/src/backend/database/routes/termix-id.ts @@ -1,6 +1,5 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { createCurrentCredentialRepository, createCurrentTermixIdentityRepository, diff --git a/src/backend/database/routes/touch-input-settings-routes.ts b/src/backend/database/routes/touch-input-settings-routes.ts new file mode 100644 index 0000000..4a78617 --- /dev/null +++ b/src/backend/database/routes/touch-input-settings-routes.ts @@ -0,0 +1,69 @@ +import type { RequestHandler, Router } from "express"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import { + normalizeTouchInputSettings, + TOUCH_INPUT_SETTING_KEY, + validateTouchInputSettingsUpdate, +} from "../../../types/touch-input-settings.js"; +import { authLogger } from "../../utils/logger.js"; +import { + createCurrentSettingsRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; + +async function readSettings() { + const raw = await createCurrentSettingsRepository().get( + TOUCH_INPUT_SETTING_KEY, + ); + if (!raw) return normalizeTouchInputSettings(null); + try { + return normalizeTouchInputSettings(JSON.parse(raw)); + } catch { + return normalizeTouchInputSettings(null); + } +} + +export function registerTouchInputSettingsRoutes( + router: Router, + authenticateJWT: RequestHandler, +): void { + router.get("/touch-input-settings", authenticateJWT, async (_req, res) => { + try { + res.json(await readSettings()); + } catch (error) { + authLogger.error("Failed to get touch input settings", error); + res.status(500).json({ error: "Failed to get touch input settings" }); + } + }); + + router.patch("/touch-input-settings", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const user = userId + ? await createCurrentUserRepository().findById(userId) + : null; + if (!user?.isAdmin) { + return res.status(403).json({ error: "Not authorized" }); + } + const validationError = validateTouchInputSettingsUpdate(req.body); + if (validationError) { + return res.status(400).json({ error: validationError }); + } + const current = await readSettings(); + const merged = { ...current, ...req.body }; + const mergedValidationError = validateTouchInputSettingsUpdate(merged); + if (mergedValidationError) { + return res.status(400).json({ error: mergedValidationError }); + } + const next = normalizeTouchInputSettings(merged); + await createCurrentSettingsRepository().set( + TOUCH_INPUT_SETTING_KEY, + JSON.stringify(next), + ); + res.json(next); + } catch (error) { + authLogger.error("Failed to update touch input settings", error); + res.status(500).json({ error: "Failed to update touch input settings" }); + } + }); +} diff --git a/src/backend/database/routes/ui-preferences.ts b/src/backend/database/routes/ui-preferences.ts new file mode 100644 index 0000000..6360fc0 --- /dev/null +++ b/src/backend/database/routes/ui-preferences.ts @@ -0,0 +1,182 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; +import express, { type Request, type Response } from "express"; +import { databaseLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { + createCurrentUiPreferenceRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; +import { + defaultUiPreferences, + sanitizeUiPreferences, + UI_ONBOARDING_VERSION, + type UiPreferences, +} from "../../../types/ui-preferences.js"; + +const router = express.Router(); +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); + +/** Accounts younger than this are treated as new and get onboarding. */ +const NEW_ACCOUNT_WINDOW_MS = 24 * 60 * 60 * 1000; + +/** + * Existing users must never be ambushed by onboarding. A user with no + * ui_preferences row who registered more than a day ago predates this feature, + * so they are handed a completed onboarding state. This is a read-time default + * rather than a migration write: nothing is persisted until the user actually + * changes something, so it stays correct on a fresh database too. + */ +function withOnboardingBackfill( + preferences: UiPreferences, + registeredAt: string | null | undefined, +): UiPreferences { + const registeredMs = registeredAt ? Date.parse(registeredAt) : Number.NaN; + const isNewAccount = + Number.isFinite(registeredMs) && + Date.now() - registeredMs < NEW_ACCOUNT_WINDOW_MS; + + if (isNewAccount) return preferences; + + return { + ...preferences, + onboarding: { + ...preferences.onboarding, + completedVersion: UI_ONBOARDING_VERSION, + }, + }; +} + +/** + * @openapi + * /ui-preferences: + * get: + * summary: Get the UI complexity preferences for the current user + * description: Returns the current user's interface preset (simple, balanced, advanced or custom), their per-area overrides, and their onboarding state. A first-time GET returns defaults without writing a row; a row is only created once the user actually changes something via PUT. Users who registered before this feature existed are returned an already-completed onboarding state so they are never shown the first-run flow. + * tags: + * - UI Preferences + * responses: + * 200: + * description: The current user's UI preferences. + */ +router.get("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const existing = + await createCurrentUiPreferenceRepository().findByUserId(userId); + + if (existing) { + return res.json({ + preferences: sanitizeUiPreferences(JSON.parse(existing.data)), + }); + } + + const user = await createCurrentUserRepository().findById(userId); + return res.json({ + preferences: withOnboardingBackfill( + defaultUiPreferences(), + user?.registeredAt, + ), + }); + } catch (e) { + databaseLogger.error("Failed to get UI preferences", e, { + operation: "get_ui_preferences", + userId, + }); + return res.status(500).json({ error: "Failed to get UI preferences" }); + } +}); + +/** + * @openapi + * /ui-preferences: + * put: + * summary: Update the UI complexity preferences for the current user + * description: Persists the current user's interface preset, per-area overrides and onboarding state as a single JSON document. Overrides are merged two levels deep, so a request only has to send the keys it changes. A null at a key clears that single override; a null at an area clears every override for that area; a null at overrides clears all of them. + * tags: + * - UI Preferences + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: Preferences updated successfully. + * 400: + * description: Invalid preferences payload. + */ +router.put("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + + if (!req.body || typeof req.body !== "object") { + return res.status(400).json({ error: "Invalid preferences payload" }); + } + + try { + const repository = createCurrentUiPreferenceRepository(); + const existing = await repository.findByUserId(userId); + const base = existing + ? sanitizeUiPreferences(JSON.parse(existing.data)) + : defaultUiPreferences(); + + const body = req.body as Record; + + // Two levels of merging, with null meaning "clear this". A plain spread + // could not express clearing an override, which the settings UI needs to + // hand a knob back to the preset. + const mergedOverrides: Record> = { + ...(base.overrides as Record>), + }; + + if (body.overrides === null) { + for (const area of Object.keys(mergedOverrides)) { + delete mergedOverrides[area]; + } + } else if (body.overrides && typeof body.overrides === "object") { + for (const [area, patch] of Object.entries( + body.overrides as Record, + )) { + if (patch === null) { + delete mergedOverrides[area]; + continue; + } + if (!patch || typeof patch !== "object") continue; + + const next = { ...(mergedOverrides[area] ?? {}) }; + for (const [key, value] of Object.entries( + patch as Record, + )) { + if (value === null) delete next[key]; + else next[key] = value; + } + mergedOverrides[area] = next; + } + } + + const merged = sanitizeUiPreferences({ + ...base, + ...body, + // Must come after the body spread, or a raw overrides payload would + // clobber the merge above. + overrides: mergedOverrides, + onboarding: { + ...base.onboarding, + ...((body.onboarding as Record) ?? {}), + }, + }); + + await repository.upsert(userId, JSON.stringify(merged)); + + return res.json({ success: true, preferences: merged }); + } catch (e) { + databaseLogger.error("Failed to update UI preferences", e, { + operation: "update_ui_preferences", + userId, + }); + return res.status(500).json({ error: "Failed to update UI preferences" }); + } +}); + +export default router; diff --git a/src/backend/database/routes/user-admin-routes.ts b/src/backend/database/routes/user-admin-routes.ts index 083c37c..fc7250f 100644 --- a/src/backend/database/routes/user-admin-routes.ts +++ b/src/backend/database/routes/user-admin-routes.ts @@ -38,13 +38,36 @@ export function registerUserAdminRoutes( * @openapi * /users/list: * get: - * summary: List all users - * description: Retrieves a list of all users in the system. + * summary: List users + * description: > + * Retrieves users in the system. Without `limit` the full list is + * returned, which is what the sharing pickers rely on. Pass `limit` + * (and optionally `offset`/`search`) to page through large directories. * tags: * - Users + * parameters: + * - in: query + * name: search + * required: false + * schema: + * type: string + * description: Case-insensitive username substring filter. + * - in: query + * name: limit + * required: false + * schema: + * type: integer + * maximum: 500 + * description: Page size. Omit to return every user. + * - in: query + * name: offset + * required: false + * schema: + * type: integer + * description: Number of users to skip. Requires `limit`. * responses: * 200: - * description: A list of users. + * description: A list of users, with the total matching count. * 403: * description: Not authorized. * 500: @@ -56,10 +79,36 @@ export function registerUserAdminRoutes( const requester = await userRepository.findById( (req as AuthenticatedRequest).userId, ); - const allUsers = await userRepository.listAll(); + + const query = req.query ?? {}; + const search = + typeof query.search === "string" ? query.search : undefined; + const rawLimit = Number(query.limit); + // Paging is opt-in: the share pickers fetch the whole list and filter it + // client-side, so a request without ?limit keeps the original behaviour. + const paginated = Number.isFinite(rawLimit) && rawLimit > 0; + const limit = paginated ? Math.min(Math.floor(rawLimit), 500) : 0; + const rawOffset = Number(query.offset); + const offset = + Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : 0; + + let pageUsers: UserRecord[]; + let total: number; + if (paginated) { + const page = await userRepository.listPage({ search, limit, offset }); + pageUsers = page.users; + total = page.total; + } else { + const all = await userRepository.listAll(); + const term = search?.trim().toLowerCase(); + pageUsers = term + ? all.filter((u) => u.username?.toLowerCase().includes(term)) + : all; + total = pageUsers.length; + } res.json({ - users: allUsers.map((u) => ({ + users: pageUsers.map((u) => ({ userId: u.id, username: u.username, is_admin: u.isAdmin, @@ -74,6 +123,8 @@ export function registerUserAdminRoutes( } : {}), })), + total, + ...(paginated ? { limit, offset } : {}), }); } catch (err) { authLogger.error("Failed to list users", err); diff --git a/src/backend/database/routes/user-image-storage-routes.ts b/src/backend/database/routes/user-image-storage-routes.ts new file mode 100644 index 0000000..4b7eacf --- /dev/null +++ b/src/backend/database/routes/user-image-storage-routes.ts @@ -0,0 +1,334 @@ +import type { Request, RequestHandler, Response, Router } from "express"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import { databaseLogger } from "../../utils/logger.js"; +import { sessionManager } from "../../hosts/terminal/session-manager.js"; +import { createCurrentSettingsRepository } from "../repositories/factory.js"; +import { + MIN_IMAGE_MAX_BYTES, + TERMINAL_IMAGE_STORAGE_KEYS, + parseImageHostPath, + parseImageLocalDir, + parseTerminalImageStorageMode, + resolveTerminalImageStorageSettings, + type TerminalImageStorageSettings, +} from "./terminal-image-storage-settings.js"; +import { + probeLocalImageVisibility, + selectImageStorageMode, +} from "./terminal-image-storage.js"; + +/** + * Admin-only terminal image storage settings. + * + * The public shape deliberately omits `localDir`: it is a backend-internal + * path and only the agent-visible `hostPath` may leave the server. No + * credentials or connection details are ever returned here. + */ +interface PublicImageStorageSettings { + mode: TerminalImageStorageSettings["mode"]; + hostPath: string; + ttlMs: number; + maxCount: number; + maxBytes: number; + localMappingConfigured: boolean; +} + +function toPublicSettings( + settings: TerminalImageStorageSettings, +): PublicImageStorageSettings { + return { + mode: settings.mode, + hostPath: settings.hostPath, + ttlMs: settings.ttlMs, + maxCount: settings.maxCount, + maxBytes: settings.maxBytes, + localMappingConfigured: settings.localMappingConfigured, + }; +} + +const PATCHABLE_FIELDS = [ + "mode", + "localDir", + "hostPath", + "ttlMs", + "maxCount", + "maxBytes", +] as const; + +type PatchableField = (typeof PATCHABLE_FIELDS)[number]; + +function invalidField(res: Response, field: string): void { + // Safe by construction: the field name is fixed, the rejected value and + // any backend path are never echoed back. + res.status(400).json({ + error: `Invalid value for ${field}`, + code: "IMAGE_STORAGE_SETTINGS_INVALID", + field, + }); +} + +function parseIntegerField(value: unknown, min: number): number | null { + if (typeof value !== "number" || !Number.isInteger(value)) return null; + if (value < min) return null; + return value; +} + +/** + * Validates a PATCH body and returns the settings-table writes it implies, or + * null after the response has already been failed with a 400. + */ +function buildImageStorageWrites( + body: unknown, + res: Response, +): Array<{ key: string; value: string }> | null { + if (typeof body !== "object" || body === null || Array.isArray(body)) { + res.status(400).json({ + error: "Invalid request body", + code: "IMAGE_STORAGE_SETTINGS_INVALID", + }); + return null; + } + + for (const field of Object.keys(body)) { + if (!(PATCHABLE_FIELDS as readonly string[]).includes(field)) { + res.status(400).json({ + error: `Unknown setting field: ${field}`, + code: "IMAGE_STORAGE_SETTINGS_UNKNOWN_FIELD", + field, + }); + return null; + } + } + + const input = body as Partial>; + const writes: Array<{ key: string; value: string }> = []; + + if (input.mode !== undefined) { + const mode = parseTerminalImageStorageMode(input.mode); + if (mode === null) { + invalidField(res, "mode"); + return null; + } + writes.push({ key: TERMINAL_IMAGE_STORAGE_KEYS.mode, value: mode }); + } + + if (input.localDir !== undefined) { + const localDir = parseImageLocalDir(input.localDir); + if (localDir === null) { + invalidField(res, "localDir"); + return null; + } + writes.push({ key: TERMINAL_IMAGE_STORAGE_KEYS.localDir, value: localDir }); + } + + if (input.hostPath !== undefined) { + const hostPath = parseImageHostPath(input.hostPath); + if (hostPath === null) { + invalidField(res, "hostPath"); + return null; + } + writes.push({ + key: TERMINAL_IMAGE_STORAGE_KEYS.hostPath, + value: hostPath, + }); + } + + if (input.ttlMs !== undefined) { + const ttlMs = parseIntegerField(input.ttlMs, 0); + if (ttlMs === null) { + invalidField(res, "ttlMs"); + return null; + } + writes.push({ + key: TERMINAL_IMAGE_STORAGE_KEYS.ttlMs, + value: String(ttlMs), + }); + } + + if (input.maxCount !== undefined) { + const maxCount = parseIntegerField(input.maxCount, 1); + if (maxCount === null) { + invalidField(res, "maxCount"); + return null; + } + writes.push({ + key: TERMINAL_IMAGE_STORAGE_KEYS.maxCount, + value: String(maxCount), + }); + } + + if (input.maxBytes !== undefined) { + const maxBytes = parseIntegerField(input.maxBytes, MIN_IMAGE_MAX_BYTES); + if (maxBytes === null) { + invalidField(res, "maxBytes"); + return null; + } + writes.push({ + key: TERMINAL_IMAGE_STORAGE_KEYS.maxBytes, + value: String(maxBytes), + }); + } + + return writes; +} + +export function registerUserImageStorageRoutes( + router: Router, + requireAdmin: RequestHandler, +): void { + /** + * @openapi + * /users/terminal-image-storage-settings: + * get: + * summary: Get terminal image storage settings (admin only) + * description: Returns the effective terminal image storage settings. The backend-internal localDir is never exposed; only the agent-visible hostPath is returned. + * tags: + * - Users + * responses: + * 200: + * description: Effective image storage settings. + * 401: + * description: Not authenticated. + * 403: + * description: Admin access required. + * 500: + * description: Failed to load settings. + */ + router.get( + "/terminal-image-storage-settings", + requireAdmin, + async (_req: Request, res: Response) => { + try { + const settings = await resolveTerminalImageStorageSettings( + createCurrentSettingsRepository(), + ); + res.json(toPublicSettings(settings)); + } catch (err) { + databaseLogger.error("Failed to load image storage settings", err); + res + .status(500) + .json({ error: "Failed to load image storage settings" }); + } + }, + ); + + /** + * @openapi + * /users/terminal-image-storage-settings: + * patch: + * summary: Update terminal image storage settings (admin only) + * description: Persists a partial update. Accepts only mode (auto, local, remote-sftp), localDir, hostPath, ttlMs, maxCount and maxBytes; invalid values are rejected with a 400. + * tags: + * - Users + * responses: + * 200: + * description: Updated effective settings. + * 400: + * description: Invalid or unknown setting field. + * 401: + * description: Not authenticated. + * 403: + * description: Admin access required. + * 500: + * description: Failed to save settings. + */ + router.patch( + "/terminal-image-storage-settings", + requireAdmin, + async (req: Request, res: Response) => { + const writes = buildImageStorageWrites(req.body, res); + if (writes === null) return; + + try { + const settings = createCurrentSettingsRepository(); + if (typeof settings.setMany !== "function") { + throw new Error("Atomic settings persistence is unavailable"); + } + await settings.setMany(writes); + const resolved = await resolveTerminalImageStorageSettings(settings); + res.json(toPublicSettings(resolved)); + } catch (err) { + databaseLogger.error("Failed to save image storage settings", err); + res + .status(500) + .json({ error: "Failed to save image storage settings" }); + } + }, + ); + + /** + * @openapi + * /users/terminal-image-storage-settings/test: + * post: + * summary: Test image storage visibility (admin only) + * description: Reports which storage mode an upload would take for one of the caller's already-connected terminal sessions. Uses the bounded local-mapping probe only; it never opens new connections. + * tags: + * - Users + * responses: + * 200: + * description: Visibility test result. + * 400: + * description: Missing terminal session instanceId. + * 401: + * description: Not authenticated. + * 403: + * description: Admin access required. + * 500: + * description: Failed to run the test. + */ + router.post( + "/terminal-image-storage-settings/test", + requireAdmin, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const instanceId = req.body?.instanceId; + if (typeof instanceId !== "string" || instanceId.trim().length === 0) { + return res.status(400).json({ + error: "Missing terminal session", + code: "IMAGE_SESSION_MISSING", + }); + } + + try { + const settings = await resolveTerminalImageStorageSettings( + createCurrentSettingsRepository(), + ); + const session = sessionManager + .getUserSessions(userId) + .find( + (candidate) => + (candidate.attachedTabInstanceId ?? candidate.tabInstanceId) === + instanceId && candidate.isConnected, + ); + const remoteSftpAvailable = !!session?.sshConn; + + let localHostVisible: boolean | null = null; + if (settings.localMappingConfigured && session?.sshConn) { + localHostVisible = await probeLocalImageVisibility( + session.sshConn, + settings, + ).catch(() => false); + } + + const selectedMode = selectImageStorageMode(settings, { + remoteSftpAvailable, + ...(localHostVisible !== null ? { localHostVisible } : {}), + }); + + res.json({ + mode: settings.mode, + connected: !!session, + remoteSftpAvailable, + localHostVisible, + selectedMode, + localMappingConfigured: settings.localMappingConfigured, + }); + } catch (err) { + databaseLogger.error("Failed to test image storage visibility", err); + res + .status(500) + .json({ error: "Failed to test image storage visibility" }); + } + }, + ); +} diff --git a/src/backend/database/routes/user-oidc-account-routes.ts b/src/backend/database/routes/user-oidc-account-routes.ts index 29fd6bd..7f72eb5 100644 --- a/src/backend/database/routes/user-oidc-account-routes.ts +++ b/src/backend/database/routes/user-oidc-account-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; import type { RequestHandler, Router } from "express"; import { AuthManager } from "../../utils/auth-manager.js"; @@ -163,7 +164,7 @@ export function registerUserOidcAccountRoutes( }); res.status(500).json({ error: "Failed to link accounts", - details: err instanceof Error ? err.message : "Unknown error", + details: getErrorMessage(err), }); } }); @@ -297,7 +298,7 @@ export function registerUserOidcAccountRoutes( }); res.status(500).json({ error: "Failed to unlink OIDC", - details: err instanceof Error ? err.message : "Unknown error", + details: getErrorMessage(err), }); } }, diff --git a/src/backend/database/routes/user-oidc-utils.ts b/src/backend/database/routes/user-oidc-utils.ts index 3b3e66e..158e37c 100644 --- a/src/backend/database/routes/user-oidc-utils.ts +++ b/src/backend/database/routes/user-oidc-utils.ts @@ -1,6 +1,7 @@ import { authLogger } from "../../utils/logger.js"; import type { SSOProviderType } from "../../../types/index.js"; import { DataCrypto } from "../../utils/data-crypto.js"; +import { decryptSsoConfigSecrets } from "../../utils/system-secret-crypto.js"; import { Agent } from "undici"; import { createCurrentSettingsRepository, @@ -10,8 +11,22 @@ import { const BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout"; +/** + * Raised when a token cannot be verified because it is not a compact JWS, + * as opposed to a signature or claim check that actually failed. + */ +export class OIDCTokenFormatError extends Error { + constructor(message: string) { + super(message); + this.name = "OIDCTokenFormatError"; + } +} + function normalizeIssuer(url: string): string { - return url.trim().replace(/\/+$/, ""); + return url + .trim() + .replace(/\/+$/, "") + .replace(/\/\.well-known\/openid-configuration$/, ""); } export type OIDCConfig = { @@ -27,6 +42,7 @@ export type OIDCConfig = { allowed_users: string; admin_group: string; group_claim?: string; + role_map?: string; ca_cert?: string; }; @@ -35,6 +51,26 @@ export function buildFetchOptions(caCert?: string): Record { return { dispatcher: new Agent({ connect: { ca: caCert } }) }; } +/** + * Renders why a fetch failed in a form an administrator can act on. + * + * undici reports every transport failure as the same "fetch failed" message + * and puts the reason that actually matters -- ENOTFOUND, ECONNREFUSED, + * UNABLE_TO_VERIFY_LEAF_SIGNATURE, a timeout -- on the cause. Reporting only + * the outer message says nothing at all. + */ +export function describeFetchFailure(error: unknown): string { + if (!(error instanceof Error)) return String(error); + const cause = (error as { cause?: unknown }).cause; + if (cause instanceof Error) { + const code = (cause as { code?: unknown }).code; + return code + ? `${error.message}: ${cause.message} (${code})` + : `${error.message}: ${cause.message}`; + } + return cause ? `${error.message}: ${String(cause)}` : error.message; +} + export function getOIDCConfigFromEnv(): OIDCConfig | null { const client_id = process.env.OIDC_CLIENT_ID; const client_secret = process.env.OIDC_CLIENT_SECRET; @@ -65,9 +101,77 @@ export function getOIDCConfigFromEnv(): OIDCConfig | null { allowed_users: process.env.OIDC_ALLOWED_USERS || "", admin_group: process.env.OIDC_ADMIN_GROUP || "", group_claim: process.env.OIDC_GROUP_CLAIM || "", + role_map: process.env.OIDC_ROLE_MAP || "", }; } +/** + * Normalizes a group name for comparison. Providers are inconsistent about + * whether they emit bare names (`devops-interns`) or full paths + * (`/devops-interns`, Keycloak's "Full group path" option), so leading slashes + * are stripped and case is ignored. + */ +function normalizeGroupName(group: string): string { + return group.trim().replace(/^\/+/, "").toLowerCase(); +} + +/** + * Parses `OIDC_ROLE_MAP` into a group -> role-name lookup. + * + * Format is a comma- or newline-separated list of `group:role` pairs, e.g. + * `devops-interns:devops-intern,devops-seniors:devops-senior`. Group keys are + * normalized via {@link normalizeGroupName}; role names are passed through + * verbatim because they must match `roles.name` exactly. + * + * Malformed entries are skipped rather than throwing โ€” a typo in one pair must + * not lock every user out of login. + */ +export function parseOidcRoleMap(raw?: string | null): Map { + const map = new Map(); + if (!raw || !raw.trim()) return map; + + for (const entry of raw.split(/[\n,]/)) { + const trimmed = entry.trim(); + if (!trimmed) continue; + + // rsplit on the last ":" so group names containing a colon still work. + const separator = trimmed.lastIndexOf(":"); + if (separator <= 0 || separator === trimmed.length - 1) continue; + + const group = normalizeGroupName(trimmed.slice(0, separator)); + const roleName = trimmed.slice(separator + 1).trim(); + if (!group || !roleName) continue; + + map.set(group, roleName); + } + + return map; +} + +/** + * Resolves which mapped roles a user should hold, given their provider groups. + * + * Returns both the `desired` roles (mapped groups the user is actually in) and + * the full set of `managed` roles (every role named in the map). Callers must + * only ever add/remove roles within `managed` โ€” roles assigned by hand in + * Termix, and the `admin`/`user` roles maintained by the admin-group sync, are + * deliberately left alone. + */ +export function resolveOidcMappedRoles( + groups: string[], + roleMap: Map, +): { desired: Set; managed: Set } { + const managed = new Set(roleMap.values()); + const desired = new Set(); + + for (const group of groups) { + const roleName = roleMap.get(normalizeGroupName(group)); + if (roleName) desired.add(roleName); + } + + return { desired, managed }; +} + /** * Extracts the list of group/role names from an OIDC userInfo payload. * @@ -104,6 +208,22 @@ export function extractOidcGroups( return []; } +/** + * OIDC providers may return group claims in the ID token, userinfo response, + * or both. Keep every verified source authoritative instead of letting a + * sparse userinfo payload overwrite claims from the ID token. + */ +export function extractOidcGroupsFromSources( + sources: Record[], + groupClaim?: string, +): string[] { + return [ + ...new Set( + sources.flatMap((source) => extractOidcGroups(source, groupClaim)), + ), + ]; +} + export function isOIDCUserAllowed( allowedUsers: string, identifier: string, @@ -149,15 +269,22 @@ export async function verifyOIDCToken( clientId: string, caCert?: string, ): Promise> { + const segments = idToken.split("."); + if (segments.length !== 3) { + throw new OIDCTokenFormatError( + segments.length === 5 + ? "Token is a JWE (encrypted). Termix cannot verify encrypted tokens; disable token encryption for this client in your OIDC provider." + : `Token is not a compact JWS: expected 3 segments, got ${segments.length}.`, + ); + } + const fetchOptions = buildFetchOptions(caCert); - const normalizedIssuerUrl = issuerUrl.endsWith("/") - ? issuerUrl.slice(0, -1) - : issuerUrl; + const configuredIssuerUrl = issuerUrl.trim().replace(/\/+$/, ""); + const normalizedIssuerUrl = normalizeIssuer(issuerUrl); const possibleIssuers = [ - issuerUrl, normalizedIssuerUrl, - issuerUrl.replace(/\/application\/o\/[^/]+$/, ""), normalizedIssuerUrl.replace(/\/application\/o\/[^/]+$/, ""), + ...(configuredIssuerUrl === normalizedIssuerUrl ? [issuerUrl] : []), ]; const jwksUrls = [ @@ -166,20 +293,30 @@ export async function verifyOIDCToken( `${normalizedIssuerUrl.replace(/\/application\/o\/[^/]+$/, "")}/.well-known/jwks.json`, ]; + // Every attempt records why it failed. Without this the only thing an + // administrator ever sees is "Failed to fetch JWKS from any URL", which + // does not distinguish an issuer URL typo from a proxy, a private CA, or + // a provider outage. + const attempts: string[] = []; + + const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; try { - const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; const discoveryResponse = await fetch(discoveryUrl, fetchOptions); - if (discoveryResponse.ok) { + if (!discoveryResponse.ok) { + attempts.push(`${discoveryUrl}: HTTP ${discoveryResponse.status}`); + } else { const discovery = (await discoveryResponse.json()) as Record< string, unknown >; - if (discovery.jwks_uri) { - jwksUrls.unshift(discovery.jwks_uri as string); + if (typeof discovery.jwks_uri === "string" && discovery.jwks_uri) { + jwksUrls.unshift(discovery.jwks_uri); + } else { + attempts.push(`${discoveryUrl}: no jwks_uri in the discovery document`); } } } catch (discoveryError) { - authLogger.error(`OIDC discovery failed: ${discoveryError}`); + attempts.push(`${discoveryUrl}: ${describeFetchFailure(discoveryError)}`); } let jwks: Record | null = null; @@ -187,26 +324,25 @@ export async function verifyOIDCToken( for (const url of jwksUrls) { try { const response = await fetch(url, fetchOptions); - if (response.ok) { - const jwksData = (await response.json()) as Record; - if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) { - jwks = jwksData; - break; - } else { - authLogger.error( - `Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`, - ); - } - } else { - // expected - non-ok response, try next URL + if (!response.ok) { + attempts.push(`${url}: HTTP ${response.status}`); + continue; } - } catch { - continue; + const jwksData = (await response.json()) as Record; + if (jwksData && Array.isArray(jwksData.keys)) { + jwks = jwksData; + break; + } + attempts.push(`${url}: response contains no "keys" array`); + } catch (error) { + attempts.push(`${url}: ${describeFetchFailure(error)}`); } } if (!jwks) { - throw new Error("Failed to fetch JWKS from any URL"); + throw new Error( + `Failed to fetch JWKS from any URL. Attempts:\n ${attempts.join("\n ")}`, + ); } if (!jwks.keys || !Array.isArray(jwks.keys)) { @@ -215,9 +351,8 @@ export async function verifyOIDCToken( ); } - const header = JSON.parse( - Buffer.from(idToken.split(".")[0], "base64").toString(), - ); + const { decodeProtectedHeader, importJWK, jwtVerify } = await import("jose"); + const header = decodeProtectedHeader(idToken); const keyId = header.kid; const publicKey = jwks.keys.find( @@ -229,8 +364,9 @@ export async function verifyOIDCToken( ); } - const { importJWK, jwtVerify } = await import("jose"); - const key = await importJWK(publicKey); + const algorithm = + typeof publicKey.alg === "string" ? publicKey.alg : header.alg; + const key = await importJWK(publicKey, algorithm); const { payload } = await jwtVerify(idToken, key, { issuer: possibleIssuers, @@ -283,30 +419,15 @@ function applyProviderDefaults( }; } -function decryptConfigSecret( +/** + * Reads the provider secrets. System-key encrypted values are decrypted; + * values still carrying a legacy base64 prefix are decoded so login keeps + * working until the provider is next saved. + */ +async function decryptConfigSecret( config: Record, -): Record { - const out = { ...config }; - for (const field of ["client_secret", "bindPassword"] as const) { - const val = out[field] as string | undefined; - if (val?.startsWith("encoded:")) { - try { - out[field] = Buffer.from(val.substring(8), "base64").toString("utf8"); - } catch { - // leave as-is - } - } else if (val?.startsWith("encrypted:")) { - // encrypted: prefix means it was encrypted with DataCrypto; without a - // userId/dataKey here we cannot decrypt it. The caller should use the - // full admin decrypt path when possible. Fall back to stripping prefix. - try { - out[field] = Buffer.from(val.substring(10), "base64").toString("utf8"); - } catch { - // leave as-is - } - } - } - return out; +): Promise> { + return decryptSsoConfigSecrets(config); } export async function loadProviderConfig( @@ -340,10 +461,10 @@ export async function loadProviderConfig( ); } } catch { - parsed = decryptConfigSecret(parsed); + parsed = await decryptConfigSecret(parsed); } } else { - parsed = decryptConfigSecret(parsed); + parsed = await decryptConfigSecret(parsed); } const providerType = row.type as SSOProviderType; const config = applyProviderDefaults( @@ -380,7 +501,7 @@ export async function loadProviderConfig( } catch { parsed = {}; } - parsed = decryptConfigSecret(parsed); + parsed = await decryptConfigSecret(parsed); const oidcProviderType = oidcRow.type as SSOProviderType; return { config: applyProviderDefaults( @@ -401,7 +522,7 @@ export async function loadProviderConfig( await createCurrentSettingsRepository().get("oidc_config"); if (legacyValue) { let config = JSON.parse(legacyValue) as Record; - config = decryptConfigSecret(config); + config = await decryptConfigSecret(config); return { config: config as unknown as OIDCConfig, providerType: "oidc", @@ -432,7 +553,7 @@ export async function resolveProviderByIssuer(issuer: string): Promise<{ } catch { continue; } - parsed = decryptConfigSecret(parsed); + parsed = await decryptConfigSecret(parsed); const providerType = row.type as SSOProviderType; const config = applyProviderDefaults( parsed as unknown as OIDCConfig, diff --git a/src/backend/database/routes/user-preferences.ts b/src/backend/database/routes/user-preferences.ts index 87b5ae7..6037f3c 100644 --- a/src/backend/database/routes/user-preferences.ts +++ b/src/backend/database/routes/user-preferences.ts @@ -1,6 +1,5 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { createCurrentUserPreferenceRepository } from "../repositories/factory.js"; @@ -8,6 +7,7 @@ import type { UserPreferenceRecord, UserPreferenceUpdate, } from "../repositories/user-preference-repository.js"; +import { isValidKeybinding } from "./keybinding-validation.js"; const router = express.Router(); const authManager = AuthManager.getInstance(); @@ -31,15 +31,35 @@ const pickPreferences = (row?: UserPreferenceRecord | null) => ({ disableUpdateCheck: row?.disableUpdateCheck ?? null, confirmTabClose: row?.confirmTabClose ?? null, hiddenRailTabs: row?.hiddenRailTabs ?? null, + aiAssistantEnabled: row?.aiAssistantEnabled ?? null, + aiReadOnlyCommands: row?.aiReadOnlyCommands ?? null, compactHostView: row?.compactHostView ?? null, statusColorScheme: row?.statusColorScheme ?? null, + customThemes: row?.customThemes ?? null, + customKeybindings: row?.customKeybindings ?? null, + terminalDefaults: row?.terminalDefaults ?? null, + rdpDefaults: row?.rdpDefaults ?? null, + terminalMacros: row?.terminalMacros ?? null, }); +const connectionDefaultFields = ["terminalDefaults", "rdpDefaults"] as const; + +export function validateDefaultsJson(value: string): boolean { + if (value.length > 32_768) return false; + try { + const parsed = JSON.parse(value); + return !!parsed && typeof parsed === "object" && !Array.isArray(parsed); + } catch { + return false; + } +} + /** * @openapi * /user-preferences: * get: * summary: Get preferences for the current user + * description: showHostTags, hostTrayOnClick, compactHostView, statusColorScheme and foldersCollapsed are legacy fields, kept here read-only for backward compatibility. The authoritative copy is GET /host-sidebar/preferences. * tags: * - User Preferences * responses: @@ -106,6 +126,14 @@ const pickPreferences = (row?: UserPreferenceRecord | null) => ({ * statusColorScheme: * type: string * nullable: true + * customThemes: + * type: string + * nullable: true + * description: JSON-encoded array of the user's saved global custom terminal themes. + * customKeybindings: + * type: string + * nullable: true + * description: JSON-encoded array of the user's custom terminal keybindings. */ router.get("/", authenticateJWT, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; @@ -128,6 +156,7 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => { * /user-preferences: * put: * summary: Update preferences for the current user + * description: showHostTags, hostTrayOnClick, compactHostView, statusColorScheme and foldersCollapsed are no longer accepted here -- they moved to PUT /host-sidebar/preferences as part of the sidebar redesign. * tags: * - User Preferences * requestBody: @@ -153,16 +182,10 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => { * type: boolean * commandPaletteEnabled: * type: boolean - * showHostTags: - * type: boolean - * hostTrayOnClick: - * type: boolean * pinAppRail: * type: boolean * expandAppRailOnHover: * type: boolean - * foldersCollapsed: - * type: boolean * confirmSnippetExecution: * type: boolean * disableUpdateCheck: @@ -171,10 +194,12 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => { * type: boolean * hiddenRailTabs: * type: string - * compactHostView: - * type: boolean - * statusColorScheme: + * customThemes: * type: string + * description: JSON-encoded array of the user's saved global custom terminal themes. + * customKeybindings: + * type: string + * description: JSON-encoded array of the user's custom terminal keybindings. * responses: * 200: * description: Preferences updated successfully. @@ -190,17 +215,19 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => { storageMode, commandAutocomplete, commandPaletteEnabled, - showHostTags, - hostTrayOnClick, pinAppRail, expandAppRailOnHover, - foldersCollapsed, confirmSnippetExecution, disableUpdateCheck, confirmTabClose, hiddenRailTabs, - compactHostView, - statusColorScheme, + aiAssistantEnabled, + aiReadOnlyCommands, + customThemes, + customKeybindings, + terminalDefaults, + rdpDefaults, + terminalMacros, } = req.body as { reopenTabsOnLogin?: boolean; theme?: string | null; @@ -210,18 +237,25 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => { storageMode?: string | null; commandAutocomplete?: boolean | null; commandPaletteEnabled?: boolean | null; - showHostTags?: boolean | null; - hostTrayOnClick?: boolean | null; pinAppRail?: boolean | null; expandAppRailOnHover?: boolean | null; - foldersCollapsed?: boolean | null; confirmSnippetExecution?: boolean | null; disableUpdateCheck?: boolean | null; confirmTabClose?: boolean | null; hiddenRailTabs?: string | null; - compactHostView?: boolean | null; - statusColorScheme?: string | null; + aiAssistantEnabled?: boolean | null; + aiReadOnlyCommands?: boolean | null; + customThemes?: string | null; + customKeybindings?: string | null; + terminalDefaults?: string | null; + rdpDefaults?: string | null; + terminalMacros?: string | null; }; + // showHostTags, hostTrayOnClick, compactHostView, statusColorScheme, + // foldersCollapsed are no longer writable here -- they moved to + // /host-sidebar/preferences as of the sidebar redesign. The columns stay + // in the table (read once as a migration seed by that route) but this + // endpoint silently ignores them if a stale client still sends them. const updates: UserPreferenceUpdate = { updatedAt: new Date().toISOString(), @@ -243,25 +277,118 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => { language, storageMode, hiddenRailTabs, - statusColorScheme, + customThemes, + customKeybindings, + terminalDefaults, + rdpDefaults, + terminalMacros, })) { if (value !== undefined && value !== null && typeof value !== "string") { return res.status(400).json({ error: `${key} must be a string` }); } } + const connectionDefaults = { + terminalDefaults, + rdpDefaults, + }; + for (const key of connectionDefaultFields) { + const value = connectionDefaults[key]; + if (value !== undefined && value !== null && !validateDefaultsJson(value)) { + return res.status(400).json({ + error: `${key} must be a JSON-encoded object of at most 32 KiB`, + }); + } + } + + if (customThemes !== undefined && customThemes !== null) { + let parsedThemes: unknown; + try { + parsedThemes = JSON.parse(customThemes); + } catch { + return res + .status(400) + .json({ error: "customThemes must be a JSON-encoded array" }); + } + if (!Array.isArray(parsedThemes) || parsedThemes.length > 100) { + return res.status(400).json({ + error: "customThemes must be a JSON array of at most 100 themes", + }); + } + const isValidTheme = (entry: unknown): boolean => + !!entry && + typeof entry === "object" && + typeof (entry as { id?: unknown }).id === "string" && + typeof (entry as { name?: unknown }).name === "string" && + !!(entry as { colors?: unknown }).colors && + typeof (entry as { colors?: unknown }).colors === "object"; + if (!parsedThemes.every(isValidTheme)) { + return res.status(400).json({ + error: "Each custom theme must have an id, name, and colors object", + }); + } + } + + if (customKeybindings !== undefined && customKeybindings !== null) { + let parsedKeybindings: unknown; + try { + parsedKeybindings = JSON.parse(customKeybindings); + } catch { + return res + .status(400) + .json({ error: "customKeybindings must be a JSON-encoded array" }); + } + if (!Array.isArray(parsedKeybindings) || parsedKeybindings.length > 200) { + return res.status(400).json({ + error: "customKeybindings must be a JSON array of at most 200 bindings", + }); + } + if (!parsedKeybindings.every(isValidKeybinding)) { + return res.status(400).json({ + error: + "Each custom keybinding must have an id, enabled flag, valid combo, and valid action", + }); + } + } + + if (terminalMacros !== undefined && terminalMacros !== null) { + let parsedMacros: unknown; + try { + parsedMacros = JSON.parse(terminalMacros); + } catch { + return res + .status(400) + .json({ error: "terminalMacros must be a JSON-encoded array" }); + } + if ( + terminalMacros.length > 512 * 1024 || + !Array.isArray(parsedMacros) || + parsedMacros.length > 100 || + !parsedMacros.every( + (macro) => + !!macro && + typeof macro === "object" && + typeof (macro as { id?: unknown }).id === "string" && + typeof (macro as { name?: unknown }).name === "string" && + Array.isArray((macro as { steps?: unknown }).steps), + ) + ) { + return res.status(400).json({ + error: "terminalMacros must contain at most 100 valid macros", + }); + } + } + const boolFields: Record = { commandAutocomplete, commandPaletteEnabled, - showHostTags, - hostTrayOnClick, pinAppRail, expandAppRailOnHover, - foldersCollapsed, confirmSnippetExecution, disableUpdateCheck, confirmTabClose, - compactHostView, + aiAssistantEnabled, + aiReadOnlyCommands, }; for (const [key, value] of Object.entries(boolFields)) { if (value !== undefined && value !== null && typeof value !== "boolean") { @@ -275,25 +402,29 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => { if (language !== undefined) updates.language = language; if (storageMode !== undefined) updates.storageMode = storageMode; if (hiddenRailTabs !== undefined) updates.hiddenRailTabs = hiddenRailTabs; + if (aiAssistantEnabled !== undefined) + updates.aiAssistantEnabled = aiAssistantEnabled; + if (aiReadOnlyCommands !== undefined) + updates.aiReadOnlyCommands = aiReadOnlyCommands; if (commandAutocomplete !== undefined) updates.commandAutocomplete = commandAutocomplete; if (commandPaletteEnabled !== undefined) updates.commandPaletteEnabled = commandPaletteEnabled; - if (showHostTags !== undefined) updates.showHostTags = showHostTags; - if (hostTrayOnClick !== undefined) updates.hostTrayOnClick = hostTrayOnClick; if (pinAppRail !== undefined) updates.pinAppRail = pinAppRail; if (expandAppRailOnHover !== undefined) updates.expandAppRailOnHover = expandAppRailOnHover; - if (foldersCollapsed !== undefined) - updates.foldersCollapsed = foldersCollapsed; if (confirmSnippetExecution !== undefined) updates.confirmSnippetExecution = confirmSnippetExecution; if (disableUpdateCheck !== undefined) updates.disableUpdateCheck = disableUpdateCheck; if (confirmTabClose !== undefined) updates.confirmTabClose = confirmTabClose; - if (compactHostView !== undefined) updates.compactHostView = compactHostView; - if (statusColorScheme !== undefined) - updates.statusColorScheme = statusColorScheme; + if (customThemes !== undefined) updates.customThemes = customThemes; + if (customKeybindings !== undefined) + updates.customKeybindings = customKeybindings; + if (terminalDefaults !== undefined) + updates.terminalDefaults = terminalDefaults; + if (rdpDefaults !== undefined) updates.rdpDefaults = rdpDefaults; + if (terminalMacros !== undefined) updates.terminalMacros = terminalMacros; if (Object.keys(updates).length === 1) { return res.status(400).json({ error: "No preferences provided" }); diff --git a/src/backend/database/routes/user-settings-routes.ts b/src/backend/database/routes/user-settings-routes.ts index 2c23c10..404a44c 100644 --- a/src/backend/database/routes/user-settings-routes.ts +++ b/src/backend/database/routes/user-settings-routes.ts @@ -7,6 +7,8 @@ import { setGlobalLogLevel, } from "../../utils/logger.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; +import { getTelemetryEnvOverride } from "../../utils/analytics.js"; +import { AI_PRIVATE_ALLOWLIST_KEY, parseAllowlist } from "../../ai/egress.js"; import { createCurrentSettingsRepository, createCurrentUserRepository, @@ -340,17 +342,22 @@ export function registerUserSettingsRoutes( * description: Masked API key or empty string if not set. * hasApiKey: * type: boolean + * apiBaseUrl: + * type: string + * description: Custom control-plane API base URL (e.g. a Headscale instance), or empty string for the Tailscale default. */ router.get("/tailscale-settings", authenticateJWT, async (_req, res) => { try { - const apiKey = - (await createCurrentSettingsRepository().get("tailscale_api_key")) ?? - ""; + const settingsRepo = createCurrentSettingsRepository(); + const apiKey = (await settingsRepo.get("tailscale_api_key")) ?? ""; + const apiBaseUrl = + (await settingsRepo.get("tailscale_api_base_url")) ?? ""; res.json({ apiKey: apiKey ? `${apiKey.slice(0, 6)}${"*".repeat(Math.max(0, apiKey.length - 6))}` : "", hasApiKey: !!apiKey, + apiBaseUrl, }); } catch (err) { authLogger.error("Failed to get Tailscale settings", err); @@ -375,6 +382,9 @@ export function registerUserSettingsRoutes( * properties: * apiKey: * type: string + * apiBaseUrl: + * type: string + * description: Optional custom control-plane API base URL (e.g. a Headscale instance). Leave empty to use the Tailscale default. * responses: * 200: * description: Tailscale settings updated. @@ -390,11 +400,21 @@ export function registerUserSettingsRoutes( if (!actor) { return res.status(403).json({ error: "Not authorized" }); } - const { apiKey } = req.body; + const { apiKey, apiBaseUrl } = req.body; if (typeof apiKey !== "string") { return res.status(400).json({ error: "apiKey must be a string" }); } - await createCurrentSettingsRepository().set("tailscale_api_key", apiKey); + if (apiBaseUrl !== undefined && typeof apiBaseUrl !== "string") { + return res.status(400).json({ error: "apiBaseUrl must be a string" }); + } + const settingsRepo = createCurrentSettingsRepository(); + await settingsRepo.set("tailscale_api_key", apiKey); + if (apiBaseUrl !== undefined) { + await settingsRepo.set( + "tailscale_api_base_url", + apiBaseUrl.trim().replace(/\/+$/, ""), + ); + } const { ipAddress, userAgent } = getRequestMeta(req); await logAudit({ @@ -519,6 +539,421 @@ export function registerUserSettingsRoutes( }, ); + /** + * @openapi + * /users/analytics-enabled: + * get: + * summary: Get analytics enabled setting + * description: Returns whether anonymous usage telemetry is enabled, and whether the value is locked by the ENABLE_TELEMETRY environment variable. + * tags: + * - Users + * responses: + * 200: + * description: Analytics enabled status. + * content: + * application/json: + * schema: + * type: object + * properties: + * enabled: + * type: boolean + * locked: + * type: boolean + */ + router.get("/analytics-enabled", authenticateJWT, async (_req, res) => { + try { + const override = getTelemetryEnvOverride(); + if (override !== null) { + return res.json({ enabled: override, locked: true }); + } + res.json({ + enabled: await createCurrentSettingsRepository().getBoolean( + "analytics_enabled", + true, + ), + locked: false, + }); + } catch (err) { + authLogger.error("Failed to get analytics enabled setting", err); + res + .status(500) + .json({ error: "Failed to get analytics enabled setting" }); + } + }); + + /** + * @openapi + * /users/analytics-enabled: + * patch: + * summary: Update analytics enabled setting (admin only) + * description: Enables or disables the daily anonymous usage telemetry heartbeat. + * tags: + * - Users + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * enabled: + * type: boolean + * responses: + * 200: + * description: Setting updated. + * 403: + * description: Not authorized. + * 409: + * description: Setting is locked by the ENABLE_TELEMETRY environment variable. + * 500: + * description: Failed to update setting. + */ + router.patch("/analytics-enabled", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const actor = await getAdminActor(userId); + if (!actor) { + return res.status(403).json({ error: "Not authorized" }); + } + if (getTelemetryEnvOverride() !== null) { + return res.status(409).json({ + error: "Telemetry is locked by the ENABLE_TELEMETRY env variable", + }); + } + const { enabled } = req.body; + if (typeof enabled !== "boolean") { + return res.status(400).json({ error: "enabled must be a boolean" }); + } + await createCurrentSettingsRepository().set( + "analytics_enabled", + enabled ? "true" : "false", + ); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: actor.username ?? userId, + action: "update_analytics_enabled", + resourceType: "setting", + details: JSON.stringify({ enabled }), + ipAddress, + userAgent, + success: true, + }); + + res.json({ enabled }); + } catch (err) { + authLogger.error("Failed to update analytics enabled setting", err); + res + .status(500) + .json({ error: "Failed to update analytics enabled setting" }); + } + }); + + /** + * @openapi + * /users/session-sharing-enabled: + * get: + * summary: Get session sharing globally enabled setting + * description: Returns whether live session sharing (terminal/RDP/VNC/Telnet share links and in-app joins) is allowed instance-wide. Overrides every per-host toggle when false. + * tags: + * - Users + * responses: + * 200: + * description: Session sharing enabled status. + * content: + * application/json: + * schema: + * type: object + * properties: + * enabled: + * type: boolean + */ + router.get("/session-sharing-enabled", authenticateJWT, async (_req, res) => { + try { + res.json({ + enabled: await createCurrentSettingsRepository().getBoolean( + "session_sharing_globally_enabled", + true, + ), + }); + } catch (err) { + authLogger.error("Failed to get session sharing enabled setting", err); + res + .status(500) + .json({ error: "Failed to get session sharing enabled setting" }); + } + }); + + /** + * @openapi + * /users/session-sharing-enabled: + * patch: + * summary: Update session sharing globally enabled setting (admin only) + * description: Enables or disables live session sharing instance-wide, overriding every per-host allowSessionSharing toggle. + * tags: + * - Users + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * enabled: + * type: boolean + * responses: + * 200: + * description: Setting updated. + * 403: + * description: Not authorized. + * 500: + * description: Failed to update setting. + */ + router.patch( + "/session-sharing-enabled", + authenticateJWT, + async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const actor = await getAdminActor(userId); + if (!actor) { + return res.status(403).json({ error: "Not authorized" }); + } + const { enabled } = req.body; + if (typeof enabled !== "boolean") { + return res.status(400).json({ error: "enabled must be a boolean" }); + } + await createCurrentSettingsRepository().set( + "session_sharing_globally_enabled", + enabled ? "true" : "false", + ); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: actor.username ?? userId, + action: "update_session_sharing_enabled", + resourceType: "setting", + details: JSON.stringify({ enabled }), + ipAddress, + userAgent, + success: true, + }); + + res.json({ enabled }); + } catch (err) { + authLogger.error( + "Failed to update session sharing enabled setting", + err, + ); + res + .status(500) + .json({ error: "Failed to update session sharing enabled setting" }); + } + }, + ); + + /** + * @openapi + * /users/ai-enabled: + * get: + * summary: Get whether the AI assistant is enabled instance-wide + * tags: + * - Users + * responses: + * 200: + * description: AI enabled status. + * content: + * application/json: + * schema: + * type: object + * properties: + * enabled: + * type: boolean + */ + router.get("/ai-enabled", authenticateJWT, async (_req, res) => { + try { + res.json({ + // Defaults to false so upgrading an install never turns the assistant + // on without an admin deciding to. + enabled: await createCurrentSettingsRepository().getBoolean( + "ai_globally_enabled", + false, + ), + }); + } catch (err) { + authLogger.error("Failed to get AI enabled setting", err); + res.status(500).json({ error: "Failed to get AI enabled setting" }); + } + }); + + /** + * @openapi + * /users/ai-enabled: + * patch: + * summary: Update the instance-wide AI assistant setting (admin only) + * description: Turning this off hides and blocks the assistant for every user, whatever their own preference says. + * tags: + * - Users + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * enabled: + * type: boolean + * responses: + * 200: + * description: Setting updated. + * 403: + * description: Not authorized. + */ + router.patch("/ai-enabled", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const actor = await getAdminActor(userId); + if (!actor) { + return res.status(403).json({ error: "Not authorized" }); + } + const { enabled } = req.body; + if (typeof enabled !== "boolean") { + return res.status(400).json({ error: "enabled must be a boolean" }); + } + await createCurrentSettingsRepository().set( + "ai_globally_enabled", + enabled ? "true" : "false", + ); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: actor.username ?? userId, + action: "update_ai_enabled", + resourceType: "setting", + details: JSON.stringify({ enabled }), + ipAddress, + userAgent, + success: true, + }); + + res.json({ enabled }); + } catch (err) { + authLogger.error("Failed to update AI enabled setting", err); + res.status(500).json({ error: "Failed to update AI enabled setting" }); + } + }); + + /** + * @openapi + * /users/ai-private-endpoints: + * get: + * summary: Get the allowlist of private AI endpoint hosts + * tags: + * - Users + * responses: + * 200: + * description: Allowed hosts. + */ + router.get("/ai-private-endpoints", authenticateJWT, async (_req, res) => { + try { + const raw = await createCurrentSettingsRepository().get( + AI_PRIVATE_ALLOWLIST_KEY, + ); + res.json({ hosts: parseAllowlist(raw) }); + } catch (err) { + authLogger.error("Failed to get AI private endpoint allowlist", err); + res.status(500).json({ error: "Failed to get the allowlist" }); + } + }); + + /** + * @openapi + * /users/ai-private-endpoints: + * patch: + * summary: Replace the allowlist of private AI endpoint hosts (admin only) + * description: > + * Providers on private or loopback addresses, such as a self-hosted + * Ollama, are refused unless their host appears here. Without this an + * ordinary user could point a provider at an internal service and use + * the server as a probe of its own network. + * tags: + * - Users + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * hosts: + * type: array + * items: + * type: string + * responses: + * 200: + * description: Allowlist updated. + * 403: + * description: Not authorized. + */ + router.patch("/ai-private-endpoints", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const actor = await getAdminActor(userId); + if (!actor) { + return res.status(403).json({ error: "Not authorized" }); + } + + const { hosts } = req.body; + if (!Array.isArray(hosts)) { + return res.status(400).json({ error: "hosts must be an array" }); + } + if (hosts.length > 50) { + return res.status(400).json({ error: "At most 50 hosts are allowed" }); + } + + const cleaned: string[] = []; + for (const entry of hosts) { + if (typeof entry !== "string") { + return res.status(400).json({ error: "Each host must be a string" }); + } + const host = entry.trim().toLowerCase(); + if (!host) continue; + // A bare host, not a URL: no scheme, path, port or whitespace. + if (!/^[a-z0-9._:-]+$/.test(host)) { + return res + .status(400) + .json({ error: `${entry} is not a valid hostname` }); + } + if (!cleaned.includes(host)) cleaned.push(host); + } + + await createCurrentSettingsRepository().set( + AI_PRIVATE_ALLOWLIST_KEY, + JSON.stringify(cleaned), + ); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: actor.username ?? userId, + action: "update_ai_private_endpoints", + resourceType: "setting", + details: JSON.stringify({ hosts: cleaned }), + ipAddress, + userAgent, + success: true, + }); + + res.json({ hosts: cleaned }); + } catch (err) { + authLogger.error("Failed to update AI private endpoint allowlist", err); + res.status(500).json({ error: "Failed to update the allowlist" }); + } + }); + /** * @openapi * /users/host-defaults: diff --git a/src/backend/database/routes/user-totp-routes.ts b/src/backend/database/routes/user-totp-routes.ts index d5df38e..2050108 100644 --- a/src/backend/database/routes/user-totp-routes.ts +++ b/src/backend/database/routes/user-totp-routes.ts @@ -9,8 +9,10 @@ import { FieldCrypto } from "../../utils/field-crypto.js"; import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js"; import { authLogger } from "../../utils/logger.js"; import { loginRateLimiter } from "../../utils/login-rate-limiter.js"; +import { isTrustedProxyAuthEnabled } from "../../utils/trusted-proxy-auth.js"; import { generateDeviceFingerprint, + getDeviceId, parseUserAgent, } from "../../utils/user-agent-parser.js"; import { @@ -182,6 +184,11 @@ export function registerUserTotpRoutes( * description: Failed to enable TOTP. */ router.post("/totp/enable", authenticateJWT, async (req, res) => { + if (isTrustedProxyAuthEnabled()) { + return res.status(409).json({ + error: "TOTP is disabled while trusted proxy authentication is enabled", + }); + } const userId = (req as AuthenticatedRequest).userId; const sessionId = (req as AuthenticatedRequest).sessionId; const { totp_code } = req.body; @@ -325,32 +332,35 @@ export function registerUserTotpRoutes( return res.status(404).json({ error: "User not found" }); } - if (!totp_code || (!userRecord.isOidc && !password)) { + // One re-authentication value, whichever kind it is. The dialog offers a + // single field -- "Enter TOTP code or password" -- so it arrives in + // whichever of the two body fields the caller happened to use. + const credential = totp_code || password; + if (!credential) { return res.status(400).json({ error: userRecord.isOidc ? "A TOTP code is required" - : "Both password and TOTP code are required", + : "A TOTP code or password is required", }); } - if ( - !userRecord.isOidc && - (!userRecord.passwordHash || - !(await bcrypt.compare(password, userRecord.passwordHash))) - ) { - return res.status(401).json({ error: "Incorrect password" }); - } - if (!userRecord.totpEnabled) { return res.status(400).json({ error: "TOTP is not enabled" }); } const userDataKey = authManager.getUserDataKey(userId); - const verified = await verifyTotpReauth( + // TOTP code or backup code first; verifyTotpReauth deliberately refuses + // the account password, so that stays a separate comparison here. + let verified = await verifyTotpReauth( userRecord, - totp_code, + credential, userDataKey, ); + + if (!verified && !userRecord.isOidc && userRecord.passwordHash) { + verified = await bcrypt.compare(credential, userRecord.passwordHash); + } + if (!verified) { return res .status(401) @@ -624,18 +634,23 @@ export function registerUserTotpRoutes( const deviceInfo = parseUserAgent(req); if (rememberMe) { - const deviceFingerprint = generateDeviceFingerprint(deviceInfo); - await authManager.addTrustedDevice( - userRecord.id, - deviceFingerprint, - deviceInfo.type, - deviceInfo.deviceInfo, + const deviceFingerprint = generateDeviceFingerprint( + deviceInfo, + getDeviceId(req), ); - authLogger.info("Device automatically trusted via Remember Me", { - operation: "totp_auto_trust", - userId: userRecord.id, - deviceType: deviceInfo.type, - }); + if (deviceFingerprint) { + await authManager.addTrustedDevice( + userRecord.id, + deviceFingerprint, + deviceInfo.type, + deviceInfo.deviceInfo, + ); + authLogger.info("Device automatically trusted via Remember Me", { + operation: "totp_auto_trust", + userId: userRecord.id, + deviceType: deviceInfo.type, + }); + } } const token = await authManager.generateJWTToken(userRecord.id, { diff --git a/src/backend/database/routes/user-webauthn-routes.ts b/src/backend/database/routes/user-webauthn-routes.ts index 2d247dc..aaaf51a 100644 --- a/src/backend/database/routes/user-webauthn-routes.ts +++ b/src/backend/database/routes/user-webauthn-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { Request, RequestHandler, Router } from "express"; import type { AuthenticationResponseJSON, @@ -18,6 +19,7 @@ import { AuthManager } from "../../utils/auth-manager.js"; import { authLogger } from "../../utils/logger.js"; import { generateDeviceFingerprint, + getDeviceId, parseUserAgent, } from "../../utils/user-agent-parser.js"; import { @@ -321,7 +323,7 @@ export function registerUserWebAuthnRoutes( authLogger.warn("WebAuthn registration failed", { operation: "webauthn_register_verify", userId, - error: error instanceof Error ? error.message : "Unknown", + error: getErrorMessage(error, "Unknown"), }); res.status(400).json({ error: "Passkey registration failed" }); } @@ -421,8 +423,7 @@ export function registerUserWebAuthnRoutes( } const response = req.body?.response as - | AuthenticationResponseJSON - | undefined; + AuthenticationResponseJSON | undefined; if (!response?.id) { return res.status(400).json({ error: "Invalid passkey response" }); } @@ -488,11 +489,13 @@ export function registerUserWebAuthnRoutes( ); if (userRecord.totpEnabled) { - const deviceFingerprint = generateDeviceFingerprint(deviceInfo); - const isTrusted = await authManager.isTrustedDevice( - userRecord.id, - deviceFingerprint, + const deviceFingerprint = generateDeviceFingerprint( + deviceInfo, + getDeviceId(req), ); + const isTrusted = deviceFingerprint + ? await authManager.isTrustedDevice(userRecord.id, deviceFingerprint) + : false; if (!isTrusted) { const tempToken = await authManager.generateJWTToken(userRecord.id, { @@ -537,7 +540,7 @@ export function registerUserWebAuthnRoutes( operation: "webauthn_auth_verify", credentialId: credential.id, userId: credential.userId, - error: error instanceof Error ? error.message : "Unknown", + error: getErrorMessage(error, "Unknown"), }); res.status(401).json({ error: "Passkey authentication failed" }); } diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index f58f34b..e0b8aee 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1,13 +1,14 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import express from "express"; +import express, { type Request, type Response } from "express"; import bcrypt from "bcryptjs"; +import crypto from "crypto"; import { nanoid } from "nanoid"; -import type { Request, Response } from "express"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { + getDeviceId, parseUserAgent, generateDeviceFingerprint, } from "../../utils/user-agent-parser.js"; @@ -18,19 +19,30 @@ import { isOidcTokenCallback, } from "../../utils/oidc-desktop-callback.js"; import { deleteUserAndRelatedData } from "./delete-user-data.js"; +import { + isLoopbackRequest, + extractBearerOrCookieToken, + resolveDesktopAutoSessionUser, +} from "./desktop-auto-session.js"; import { shouldShowDonationModal } from "./donation-modal-utils.js"; +import { PermissionManager } from "../../utils/permission-manager.js"; import { getOIDCConfigFromEnv, isOIDCUserAllowed, + OIDCTokenFormatError, verifyOIDCToken, - extractOidcGroups, + extractOidcGroupsFromSources, + parseOidcRoleMap, + resolveOidcMappedRoles, loadProviderConfig, buildFetchOptions, resolveProviderByIssuer, validateLogoutToken, } from "./user-oidc-utils.js"; import { registerUserApiKeyRoutes } from "./user-api-key-routes.js"; +import { registerUserImageStorageRoutes } from "./user-image-storage-routes.js"; import { registerUserSettingsRoutes } from "./user-settings-routes.js"; +import { registerTouchInputSettingsRoutes } from "./touch-input-settings-routes.js"; import { registerAcmeSSLRoutes } from "./acme-ssl-routes.js"; import { registerUserTotpRoutes } from "./user-totp-routes.js"; import { registerUserWebAuthnRoutes } from "./user-webauthn-routes.js"; @@ -42,18 +54,35 @@ import { registerUserDataAccessRoutes } from "./user-data-access-routes.js"; import { registerSSOProviderRoutes } from "./sso-provider-routes.js"; import { registerLDAPAuthRoutes } from "./ldap-auth-routes.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; +import { notifyAutomationInternalEvent } from "../../hosts/metrics/automation-bridge.js"; import { createCurrentSettingsRepository, getCurrentSettingValue, createCurrentRoleRepository, + createCurrentSsoProviderRepository, createCurrentUserRepository, } from "../repositories/factory.js"; import type { UserRecord } from "../repositories/user-repository.js"; +import { + getTrustedProxyAuthConfig, + isTrustedProxyAddress, + isTrustedProxyAuthEnabled, + resolveTrustedProxyRoles, +} from "../../utils/trusted-proxy-auth.js"; const authManager = AuthManager.getInstance(); const router = express.Router(); +router.use((req, res, next) => { + if (isTrustedProxyAuthEnabled() && req.path.startsWith("/oidc")) { + return res.status(409).json({ + error: "OIDC is disabled while trusted proxy authentication is enabled", + }); + } + next(); +}); + async function syncSharedCredentialsForUserRoles( userId: string, operation: string, @@ -124,6 +153,15 @@ async function requireCurrentAdmin(userId: string): Promise { return user?.isAdmin ? user : null; } +// RFC 7636 PKCE: 43-128 char unreserved-character string. +function generatePkceCodeVerifier(): string { + return crypto.randomBytes(64).toString("base64url"); +} + +function generatePkceCodeChallenge(verifier: string): string { + return crypto.createHash("sha256").update(verifier).digest("base64url"); +} + async function deleteOIDCStateSettings(state: string): Promise { const settingsRepository = createCurrentSettingsRepository(); await settingsRepository.delete(`oidc_state_${state}`); @@ -131,6 +169,7 @@ async function deleteOIDCStateSettings(state: string): Promise { await settingsRepository.delete(`oidc_frontend_origin_${state}`); await settingsRepository.delete(`oidc_remember_me_${state}`); await settingsRepository.delete(`oidc_provider_${state}`); + await settingsRepository.delete(`oidc_pkce_verifier_${state}`); } const authenticateJWT = authManager.createAuthMiddleware(); @@ -672,6 +711,9 @@ router.get("/oidc/authorize", async (req, res) => { frontendOrigin = origin; } + const codeVerifier = generatePkceCodeVerifier(); + const codeChallenge = generatePkceCodeChallenge(codeVerifier); + const settingsRepository = createCurrentSettingsRepository(); await settingsRepository.set(`oidc_state_${state}`, nonce); await settingsRepository.set( @@ -686,6 +728,7 @@ router.get("/oidc/authorize", async (req, res) => { `oidc_remember_me_${state}`, rememberMe === "true" ? "true" : "false", ); + await settingsRepository.set(`oidc_pkce_verifier_${state}`, codeVerifier); if (providerDbId != null) { await settingsRepository.set( @@ -701,6 +744,8 @@ router.get("/oidc/authorize", async (req, res) => { authUrl.searchParams.set("scope", config.scopes); authUrl.searchParams.set("state", state); authUrl.searchParams.set("nonce", nonce); + authUrl.searchParams.set("code_challenge", codeChallenge); + authUrl.searchParams.set("code_challenge_method", "S256"); res.json({ auth_url: authUrl.toString(), state, nonce }); } catch (err) { @@ -740,6 +785,9 @@ router.get("/oidc/callback", async (req, res) => { const storedRememberMeValue = await settingsRepository.get( `oidc_remember_me_${state}`, ); + const storedCodeVerifier = await settingsRepository.get( + `oidc_pkce_verifier_${state}`, + ); if (!storedBackendCallback || !storedFrontendOrigin) { return res @@ -981,6 +1029,7 @@ router.get("/oidc/callback", async (req, res) => { client_secret: config.client_secret, code: code, redirect_uri: backendCallbackUri, + ...(storedCodeVerifier ? { code_verifier: storedCodeVerifier } : {}), }), ...fetchOptions, }); @@ -1005,6 +1054,7 @@ router.get("/oidc/callback", async (req, res) => { await deleteOIDCStateSettings(state); let userInfo: Record = null; + const oidcClaimSources: Record[] = []; const userInfoUrls: string[] = []; const normalizedIssuerUrl = config.issuer_url.endsWith("/") @@ -1044,20 +1094,38 @@ router.get("/oidc/callback", async (req, res) => { ); if (tokenData.id_token) { - userInfo = await verifyOIDCToken( - tokenData.id_token as string, - config.issuer_url, - config.client_id, - caCert, - ); + try { + userInfo = await verifyOIDCToken( + tokenData.id_token as string, + config.issuer_url, + config.client_id, + caCert, + ); - const expectedNonce = storedNonce; - if (userInfo.nonce !== expectedNonce) { - authLogger.warn("OIDC ID token nonce mismatch", { - operation: "oidc_nonce_mismatch", - providerId: callbackProviderId, - }); - return res.status(401).json({ error: "Invalid OIDC token nonce" }); + const expectedNonce = storedNonce; + if (userInfo.nonce !== expectedNonce) { + authLogger.warn("OIDC ID token nonce mismatch", { + operation: "oidc_nonce_mismatch", + providerId: callbackProviderId, + }); + return res.status(401).json({ error: "Invalid OIDC token nonce" }); + } + oidcClaimSources.push(userInfo); + } catch (error) { + // A token we cannot parse as a JWS carries no claims we could trust, so + // fall through to the userinfo endpoint instead of failing the login. + // Signature and claim failures still reject: those are real rejections. + if (!(error instanceof OIDCTokenFormatError)) throw error; + + userInfo = null; + authLogger.warn( + "OIDC ID token cannot be verified, falling back to userinfo endpoint", + { + operation: "oidc_id_token_unverifiable", + providerId: callbackProviderId, + reason: error.message, + }, + ); } } @@ -1076,6 +1144,7 @@ router.get("/oidc/callback", async (req, res) => { string, unknown >; + oidcClaimSources.push(fetchedUserInfo); userInfo = { ...userInfo, ...fetchedUserInfo }; break; } else { @@ -1280,8 +1349,8 @@ router.get("/oidc/callback", async (req, res) => { // Sync admin status based on OIDC group membership if (config.admin_group) { - const groups = extractOidcGroups( - userInfo as Record, + const groups = extractOidcGroupsFromSources( + oidcClaimSources, config.group_claim, ); @@ -1326,6 +1395,89 @@ router.get("/oidc/callback", async (req, res) => { } } + // Sync RBAC roles from provider group membership (OIDC_ROLE_MAP). + // + // This is what makes environment-scoped access work without hand-assigning + // roles: map a provider group to a Termix role, grant that role access to a + // set of hosts once, and membership follows the identity provider. + // + // Only roles named in the map are touched. Roles assigned by hand, and the + // admin/user pair maintained by the admin-group sync above, are never + // removed here โ€” otherwise this would fight that block on every login. + // + // Non-fatal by design: a role-sync failure must not block a valid login. + try { + const roleMap = parseOidcRoleMap( + config.role_map ?? process.env.OIDC_ROLE_MAP, + ); + + if (roleMap.size > 0) { + const groups = extractOidcGroupsFromSources( + oidcClaimSources, + config.group_claim, + ); + const { desired, managed } = resolveOidcMappedRoles(groups, roleMap); + + const roleRepository = createCurrentRoleRepository(); + const currentRoles = await roleRepository.listUserRoles(userRecord.id); + const currentNames = new Set(currentRoles.map((r) => r.roleName)); + + const toAdd = [...desired].filter((name) => !currentNames.has(name)); + const toRemove = currentRoles.filter( + (r) => managed.has(r.roleName) && !desired.has(r.roleName), + ); + + authLogger.info( + `Evaluating OIDC role map sync. parsedGroups: ${JSON.stringify(groups)}, desiredRoles: ${JSON.stringify([...desired])}, managedRoles: ${JSON.stringify([...managed])}, groupClaim: ${config.group_claim || "(default)"}`, + { + operation: "oidc_role_map_sync_eval", + userId: userRecord.id, + }, + ); + + for (const roleName of toAdd) { + const assigned = await roleRepository.assignRoleNameToUser({ + userId: userRecord.id, + roleName, + grantedBy: userRecord.id, + }); + if (!assigned) { + authLogger.warn( + "OIDC role map references a role that does not exist", + { + operation: "oidc_role_map_missing_role", + userId: userRecord.id, + roleName, + }, + ); + } + } + + for (const role of toRemove) { + await roleRepository.removeRoleFromUser(userRecord.id, role.roleId); + } + + if (toAdd.length > 0 || toRemove.length > 0) { + authLogger.info("OIDC roles synced from group membership", { + operation: "oidc_role_map_sync", + userId: userRecord.id, + added: toAdd, + removed: toRemove.map((r) => r.roleName), + }); + // Host access is resolved through cached role permissions; drop the + // cache so the new roles apply to this session immediately. + PermissionManager.getInstance().invalidateUserPermissionCache( + userRecord.id, + ); + } + } + } catch (roleSyncError) { + authLogger.error("Failed to sync OIDC roles", roleSyncError, { + operation: "oidc_role_map_sync_failed", + userId: userRecord.id, + }); + } + try { await authManager.authenticateOIDCUser(userRecord.id, deviceInfo.type); } catch (setupError) { @@ -1423,7 +1575,179 @@ router.get("/oidc/callback", async (req, res) => { * 500: * description: Login failed. */ +router.post("/proxy-login", async (req, res) => { + let config; + try { + config = getTrustedProxyAuthConfig(); + } catch (error) { + authLogger.error( + "Invalid trusted proxy authentication configuration", + error, + ); + return res + .status(503) + .json({ error: "Proxy authentication is misconfigured" }); + } + if (!config.enabled) return res.json({ enabled: false }); + + const sourceAddress = req.socket.remoteAddress; + try { + if (!isTrustedProxyAddress(sourceAddress, config.trustedProxies)) { + authLogger.warn( + "Rejected proxy authentication from an untrusted source", + { + operation: "trusted_proxy_auth_rejected", + sourceAddress, + }, + ); + return res.status(403).json({ error: "Untrusted authentication proxy" }); + } + } catch (error) { + authLogger.error("Invalid trusted proxy allowlist", error); + return res + .status(503) + .json({ error: "Proxy authentication is misconfigured" }); + } + + const usernameValue = req.headers[config.usernameHeader]; + const roleValue = req.headers[config.roleHeader]; + const username = Array.isArray(usernameValue) + ? usernameValue[0] + : usernameValue; + const roleHeader = Array.isArray(roleValue) ? roleValue[0] : roleValue; + if (!isNonEmptyString(username) || !isNonEmptyString(roleHeader)) { + return res + .status(401) + .json({ error: "Proxy authentication headers are missing" }); + } + + const mappedRoles = resolveTrustedProxyRoles(roleHeader, config.roleMap); + if (!mappedRoles) { + return res.status(403).json({ error: "Proxy role is not mapped" }); + } + + try { + const [legacyOidc, enabledProviders, userRecord] = await Promise.all([ + createCurrentSettingsRepository().get("oidc_config"), + createCurrentSsoProviderRepository().listEnabled(), + createCurrentUserRepository().findByUsername(username), + ]); + const hasOidc = + Boolean(getOIDCConfigFromEnv() || legacyOidc) || + enabledProviders.some((provider) => + ["oidc", "github", "google"].includes(provider.type), + ); + if (hasOidc) { + return res + .status(409) + .json({ error: "Proxy authentication cannot be used with OIDC" }); + } + if (!userRecord) { + return res.status(403).json({ error: "Proxy user must already exist" }); + } + if (userRecord.isOidc || userRecord.totpEnabled) { + return res.status(409).json({ + error: "Proxy authentication cannot be used with OIDC or TOTP users", + }); + } + + const roleRepository = createCurrentRoleRepository(); + const managedRoles = new Set([...config.roleMap.values()].flat()); + for (const roleName of managedRoles) { + if (!(await roleRepository.findRoleByName(roleName))) { + authLogger.error("Trusted proxy role map references a missing role", { + operation: "trusted_proxy_auth_missing_role", + roleName, + }); + return res + .status(503) + .json({ error: "Proxy role mapping is misconfigured" }); + } + } + + const currentRoles = await roleRepository.listUserRoles(userRecord.id); + const currentNames = new Set(currentRoles.map((role) => role.roleName)); + for (const roleName of mappedRoles) { + if (!currentNames.has(roleName)) { + await roleRepository.assignRoleNameToUser({ + userId: userRecord.id, + roleName, + grantedBy: userRecord.id, + }); + } + } + for (const role of currentRoles) { + if ( + managedRoles.has(role.roleName) && + !mappedRoles.includes(role.roleName) + ) { + await roleRepository.removeRoleFromUser(userRecord.id, role.roleId); + } + } + PermissionManager.getInstance().invalidateUserPermissionCache( + userRecord.id, + ); + + const deviceInfo = parseUserAgent(req); + if ( + !(await authManager.authenticateWebAuthnUser( + userRecord.id, + deviceInfo.type, + )) + ) { + return res + .status(409) + .json({ error: "User encryption data is unavailable" }); + } + await syncSharedCredentialsForUserRoles( + userRecord.id, + "trusted_proxy_login_role_shared_credentials", + ); + const token = await authManager.generateJWTToken(userRecord.id, { + deviceType: deviceInfo.type, + deviceInfo: deviceInfo.deviceInfo, + }); + const payload = await authManager.verifyJWTToken(token); + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId: userRecord.id, + username: userRecord.username, + action: "trusted_proxy_login", + resourceType: "session", + ipAddress, + userAgent, + success: true, + }); + authLogger.success("Trusted proxy login successful", { + operation: "trusted_proxy_login", + userId: userRecord.id, + sessionId: payload?.sessionId, + mappedRoles, + }); + + return res + .cookie("jwt", token, authManager.getSecureCookieOptions(req)) + .json({ + enabled: true, + success: true, + username: userRecord.username, + userId: userRecord.id, + is_admin: !!userRecord.isAdmin, + ...(isNativeAppRequest(req) ? { token } : {}), + }); + } catch (error) { + authLogger.error("Trusted proxy login failed", error); + return res.status(500).json({ error: "Proxy authentication failed" }); + } +}); + router.post("/login", async (req, res) => { + if (isTrustedProxyAuthEnabled()) { + return res.status(403).json({ + error: + "Password login is disabled while trusted proxy authentication is enabled", + }); + } const { username, password, rememberMe } = req.body; const clientIp = req.ip || req.socket.remoteAddress || "unknown"; authLogger.info("User login request received", { @@ -1534,13 +1858,15 @@ router.post("/login", async (req, res) => { ); if (userRecord.totpEnabled) { - const deviceFingerprint = generateDeviceFingerprint(deviceInfo); - - const isTrusted = await authManager.isTrustedDevice( - userRecord.id, - deviceFingerprint, + const deviceFingerprint = generateDeviceFingerprint( + deviceInfo, + getDeviceId(req), ); + const isTrusted = deviceFingerprint + ? await authManager.isTrustedDevice(userRecord.id, deviceFingerprint) + : false; + if (isTrusted) { authLogger.info("TOTP bypassed for trusted device", { operation: "totp_bypass", @@ -1588,6 +1914,11 @@ router.post("/login", async (req, res) => { success: true, }); + notifyAutomationInternalEvent("user_login", userRecord.id, undefined, { + username, + ipAddress: loginIp, + }); + const response: Record = { success: true, is_admin: !!userRecord.isAdmin, @@ -1848,9 +2179,13 @@ router.post( * description: Not authenticated. */ router.get("/me/token", authenticateJWT, (req: Request, res: Response) => { - const token = (req as Request & { cookies: Record }).cookies - ?.jwt; - res.json({ token: token || null }); + // authenticateJWT accepts either the jwt cookie or an Authorization: + // Bearer header (see auth-manager.ts's createAuthMiddleware) -- this must + // check both too, or a request that only carried the header (e.g. the + // Electron renderer's own axios interceptor, which always attaches a + // stored localStorage JWT as a Bearer header) would pass authentication + // here but still get back a null token. + res.json({ token: extractBearerOrCookieToken(req) ?? null }); }); /** @@ -1880,6 +2215,80 @@ router.get("/setup-required", async (req, res) => { } }); +/** + * @openapi + * /users/internal/auto-session: + * post: + * summary: Mint a session for the sole local desktop user + * description: Used by the Electron desktop app to skip the login form entirely when running standalone against the embedded local backend. Only available over loopback. Logs in as the sole local user regardless of its credentials; if the local database has more than one user (e.g. repeated manual registration), deterministically logs in as the admin account, or the earliest-registered account if none is admin -- a login form must never appear for the local backend under any circumstance. Only declines if zero local users exist at all, which normal desktop provisioning never produces. Provisions the resolved user's data-encryption key if missing before minting the session, matching every other login path -- self-heals an account that previously ended up with a valid session but no usable encryption key. + * tags: + * - Users + * responses: + * 200: + * description: Session created. + * 403: + * description: Forbidden, or no local users exist. + * 500: + * description: Failed to create session. + */ +router.post("/internal/auto-session", async (req, res) => { + try { + if (!isLoopbackRequest(req)) { + authLogger.warn( + "Rejected non-loopback attempt to access auto-session endpoint", + { source: req.ip }, + ); + return res.status(403).json({ error: "Forbidden" }); + } + + const userRepository = createCurrentUserRepository(); + const allUsers = await userRepository.listAll(); + const userRecord = resolveDesktopAutoSessionUser(allUsers); + if (!userRecord) { + return res.status(403).json({ + error: "No local users exist", + }); + } + await authManager.registerUser(userRecord.id); + const existingToken = extractBearerOrCookieToken(req); + if (existingToken) { + const existingPayload = await authManager.verifyJWTToken(existingToken); + if (existingPayload?.userId === userRecord.id) { + return res.json({ + success: true, + is_admin: !!userRecord.isAdmin, + username: userRecord.username, + token: existingToken, + }); + } + } + + const token = await authManager.generateJWTToken(userRecord.id, { + deviceType: "desktop", + deviceInfo: "Termix Desktop (local)", + rememberMe: true, + }); + + const response = { + success: true, + is_admin: !!userRecord.isAdmin, + username: userRecord.username, + token, + }; + + return res + .cookie( + "jwt", + token, + authManager.getSecureCookieOptions(req, 30 * 24 * 60 * 60 * 1000), + ) + .json(response); + } catch (err) { + authLogger.error("Failed to create auto-session", err); + res.status(500).json({ error: "Failed to create auto-session" }); + } +}); + /** * @openapi * /users/count: @@ -2599,9 +3008,11 @@ registerUserOidcAccountRoutes(router, { }); registerUserSettingsRoutes(router, authenticateJWT); +registerTouchInputSettingsRoutes(router, authenticateJWT); registerAcmeSSLRoutes(router, authenticateJWT); registerUserApiKeyRoutes(router, requireAdmin); +registerUserImageStorageRoutes(router, requireAdmin); registerSSOProviderRoutes(router); registerLDAPAuthRoutes(router); diff --git a/src/backend/database/routes/vault.ts b/src/backend/database/routes/vault.ts index d70b305..55e915c 100644 --- a/src/backend/database/routes/vault.ts +++ b/src/backend/database/routes/vault.ts @@ -1,8 +1,8 @@ -import express from "express"; -import type { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import { createCurrentVaultProfileRepository, createCurrentUserRepository, + createCurrentSyncTombstoneRepository, } from "../repositories/factory.js"; import type { VaultProfileUpdateInput } from "../repositories/vault-profile-repository.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; @@ -92,11 +92,19 @@ router.get("/oidc/callback", async (req: Request, res: Response) => { const code = String(req.query.code || ""); const oidcError = req.query.error ? String(req.query.error) : ""; + const esc = (value: string): string => + value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + const html = (title: string, message: string) => - `${title} + `${esc(title)} -

${title}

${message}

+

${esc(title)}

${esc(message)}

`; if (oidcError) { @@ -421,7 +429,14 @@ router.delete( .status(403) .json({ error: "Only the owner can delete this profile" }); } - await repository.deleteById(id); + const deleted = await repository.deleteById(id); + if (deleted?.syncId) { + await createCurrentSyncTombstoneRepository().record( + userId, + "vaultProfiles", + deleted.syncId, + ); + } res.json({ success: true }); } catch (err) { authLogger.error("Failed to delete vault profile", err); diff --git a/src/backend/database/routes/workspaces.ts b/src/backend/database/routes/workspaces.ts new file mode 100644 index 0000000..fcf166a --- /dev/null +++ b/src/backend/database/routes/workspaces.ts @@ -0,0 +1,638 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; +import express, { type Request, type Response } from "express"; +import { databaseLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { createCurrentWorkspaceRepository } from "../repositories/factory.js"; +import type { WorkspaceRecord } from "../repositories/workspace-repository.js"; + +const router = express.Router(); + +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); +const requireDataAccess = authManager.createDataAccessMiddleware(); + +function isNonEmptyString(val: unknown): val is string { + return typeof val === "string" && val.trim().length > 0; +} + +function parseWorkspaceId(raw: unknown): number | null { + const id = typeof raw === "string" ? parseInt(raw, 10) : NaN; + return Number.isInteger(id) ? id : null; +} + +function isValidPayload(val: unknown): val is Record { + return ( + typeof val === "object" && + val !== null && + Array.isArray((val as Record).tabs) + ); +} + +function serialize(record: WorkspaceRecord) { + let payload: unknown; + try { + payload = JSON.parse(record.payload || "{}"); + } catch { + payload = { version: 1, tabs: [] }; + } + const tabs = Array.isArray((payload as { tabs?: unknown[] })?.tabs) + ? (payload as { tabs: unknown[] }).tabs + : []; + + return { + ...record, + payload, + tabCount: tabs.length, + }; +} + +/** + * @openapi + * /workspaces: + * get: + * summary: List the current user's saved workspaces + * description: Returns every manual workspace plus the single auto-maintained "Last Session" workspace, each with a computed tabCount. + * tags: + * - Workspaces + * responses: + * 200: + * description: List of workspaces. + */ +router.get( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + + try { + const records = + await createCurrentWorkspaceRepository().listByUser(userId); + res.json(records.map(serialize)); + } catch (err) { + databaseLogger.error("Failed to list workspaces", err, { + operation: "workspace_list_failed", + userId, + }); + res.status(500).json({ error: "Failed to list workspaces" }); + } + }, +); + +/** + * @openapi + * /workspaces: + * post: + * summary: Save the current tab arrangement as a new named workspace + * tags: + * - Workspaces + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * color: + * type: string + * icon: + * type: string + * payload: + * type: object + * responses: + * 200: + * description: Workspace created. + * 400: + * description: Invalid request body. + */ +router.post( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { name, color, icon, payload } = req.body ?? {}; + + if (!isNonEmptyString(name)) { + return res.status(400).json({ error: "Workspace name is required" }); + } + if (!isValidPayload(payload)) { + return res + .status(400) + .json({ error: "payload with a tabs array is required" }); + } + + try { + const created = await createCurrentWorkspaceRepository().create(userId, { + name: name.trim(), + color: isNonEmptyString(color) ? color : null, + icon: isNonEmptyString(icon) ? icon : null, + payload: JSON.stringify(payload), + }); + res.json(serialize(created)); + } catch (err) { + databaseLogger.error("Failed to create workspace", err, { + operation: "workspace_create_failed", + userId, + }); + res.status(500).json({ error: "Failed to create workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/last-session: + * get: + * summary: Fetch the auto-maintained "Last Session" workspace + * description: Returns null if the current session has never been auto-saved yet. + * tags: + * - Workspaces + * responses: + * 200: + * description: The Last Session workspace, or null. + */ +router.get( + "/last-session", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + + try { + const record = + await createCurrentWorkspaceRepository().findLastSession(userId); + res.json(record ? serialize(record) : null); + } catch (err) { + databaseLogger.error("Failed to fetch last session workspace", err, { + operation: "workspace_last_session_get_failed", + userId, + }); + res.status(500).json({ error: "Failed to fetch last session workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/last-session: + * put: + * summary: Upsert the auto-maintained "Last Session" workspace + * description: Always overwrites the single Last Session row for the caller - never creates a second one. Called by the frontend's debounced auto-save effect. + * tags: + * - Workspaces + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * payload: + * type: object + * responses: + * 200: + * description: Last Session workspace saved. + * 400: + * description: Invalid request body. + */ +router.put( + "/last-session", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { payload } = req.body ?? {}; + + if (!isValidPayload(payload)) { + return res + .status(400) + .json({ error: "payload with a tabs array is required" }); + } + + try { + const saved = await createCurrentWorkspaceRepository().upsertLastSession( + userId, + JSON.stringify(payload), + ); + res.json(serialize(saved)); + } catch (err) { + databaseLogger.error("Failed to save last session workspace", err, { + operation: "workspace_last_session_save_failed", + userId, + }); + res.status(500).json({ error: "Failed to save last session workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/{id}: + * patch: + * summary: Rename or recolor a workspace + * description: Does not accept a payload - use PUT /workspaces/{id}/content to overwrite a workspace's saved tab arrangement. Rejects the Last Session workspace. + * tags: + * - Workspaces + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Workspace updated. + * 400: + * description: Invalid request, or target is the Last Session workspace. + * 404: + * description: Workspace not found. + */ +router.patch( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseWorkspaceId(req.params.id); + if (id === null) { + return res.status(400).json({ error: "Invalid workspace ID" }); + } + + const { name, color, icon } = req.body ?? {}; + if (name !== undefined && !isNonEmptyString(name)) { + return res.status(400).json({ error: "Workspace name cannot be empty" }); + } + + try { + const updated = await createCurrentWorkspaceRepository().update( + userId, + id, + { + name: name !== undefined ? name.trim() : undefined, + color, + icon, + }, + ); + + if (!updated) { + return res.status(404).json({ error: "Workspace not found" }); + } + res.json(serialize(updated)); + } catch (err) { + databaseLogger.error("Failed to update workspace", err, { + operation: "workspace_update_failed", + userId, + workspaceId: id, + }); + res.status(500).json({ error: "Failed to update workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/{id}/content: + * put: + * summary: Overwrite a workspace's saved tab arrangement with a new payload + * description: Used by "Update with current" in the Workspaces panel. Rejects the Last Session workspace. + * tags: + * - Workspaces + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * payload: + * type: object + * responses: + * 200: + * description: Workspace content updated. + * 400: + * description: Invalid request body. + * 404: + * description: Workspace not found. + */ +router.put( + "/:id/content", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseWorkspaceId(req.params.id); + if (id === null) { + return res.status(400).json({ error: "Invalid workspace ID" }); + } + + const { payload } = req.body ?? {}; + if (!isValidPayload(payload)) { + return res + .status(400) + .json({ error: "payload with a tabs array is required" }); + } + + try { + const updated = await createCurrentWorkspaceRepository().updateContent( + userId, + id, + JSON.stringify(payload), + ); + + if (!updated) { + return res.status(404).json({ error: "Workspace not found" }); + } + res.json(serialize(updated)); + } catch (err) { + databaseLogger.error("Failed to update workspace content", err, { + operation: "workspace_content_update_failed", + userId, + workspaceId: id, + }); + res.status(500).json({ error: "Failed to update workspace content" }); + } + }, +); + +/** + * @openapi + * /workspaces/{id}/duplicate: + * post: + * summary: Duplicate a workspace's content and color/icon under a new name + * tags: + * - Workspaces + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * responses: + * 200: + * description: New workspace created from the duplicate. + * 404: + * description: Workspace not found. + */ +router.post( + "/:id/duplicate", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseWorkspaceId(req.params.id); + const { name } = req.body ?? {}; + if (id === null) { + return res.status(400).json({ error: "Invalid workspace ID" }); + } + if (!isNonEmptyString(name)) { + return res.status(400).json({ error: "Workspace name is required" }); + } + + try { + const repository = createCurrentWorkspaceRepository(); + const source = await repository.findById(userId, id); + if (!source) { + return res.status(404).json({ error: "Workspace not found" }); + } + + const created = await repository.create(userId, { + name: name.trim(), + color: source.color, + icon: source.icon, + payload: source.payload, + }); + res.json(serialize(created)); + } catch (err) { + databaseLogger.error("Failed to duplicate workspace", err, { + operation: "workspace_duplicate_failed", + userId, + workspaceId: id, + }); + res.status(500).json({ error: "Failed to duplicate workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/{id}/set-default: + * post: + * summary: Mark a workspace as the restore-on-login default + * description: Clears isDefault on any other workspace for the caller. Idempotent if the target is already the default. Rejects the Last Session workspace. + * tags: + * - Workspaces + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Workspace set as default. + * 404: + * description: Workspace not found. + */ +router.post( + "/:id/set-default", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseWorkspaceId(req.params.id); + if (id === null) { + return res.status(400).json({ error: "Invalid workspace ID" }); + } + + try { + const updated = await createCurrentWorkspaceRepository().setDefault( + userId, + id, + ); + if (!updated) { + return res.status(404).json({ error: "Workspace not found" }); + } + res.json(serialize(updated)); + } catch (err) { + databaseLogger.error("Failed to set default workspace", err, { + operation: "workspace_set_default_failed", + userId, + workspaceId: id, + }); + res.status(500).json({ error: "Failed to set default workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/{id}/unset-default: + * post: + * summary: Remove a workspace as the restore-on-login default + * description: Idempotent if the target is not currently the default. Rejects the Last Session workspace. + * tags: + * - Workspaces + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Workspace unset as default. + * 404: + * description: Workspace not found. + */ +router.post( + "/:id/unset-default", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseWorkspaceId(req.params.id); + if (id === null) { + return res.status(400).json({ error: "Invalid workspace ID" }); + } + + try { + const updated = await createCurrentWorkspaceRepository().unsetDefault( + userId, + id, + ); + if (!updated) { + return res.status(404).json({ error: "Workspace not found" }); + } + res.json(serialize(updated)); + } catch (err) { + databaseLogger.error("Failed to unset default workspace", err, { + operation: "workspace_unset_default_failed", + userId, + workspaceId: id, + }); + res.status(500).json({ error: "Failed to unset default workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/{id}/apply: + * post: + * summary: Fetch a workspace to apply and mark it as just used + * description: Returns the full workspace with its payload parsed, and touches lastUsedAt server-side so the caller does not need a second round trip. + * tags: + * - Workspaces + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The workspace to apply. + * 404: + * description: Workspace not found. + */ +router.post( + "/:id/apply", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseWorkspaceId(req.params.id); + if (id === null) { + return res.status(400).json({ error: "Invalid workspace ID" }); + } + + try { + const repository = createCurrentWorkspaceRepository(); + const record = await repository.findById(userId, id); + if (!record) { + return res.status(404).json({ error: "Workspace not found" }); + } + + await repository.touchLastUsed(userId, id); + res.json(serialize(record)); + } catch (err) { + databaseLogger.error("Failed to apply workspace", err, { + operation: "workspace_apply_failed", + userId, + workspaceId: id, + }); + res.status(500).json({ error: "Failed to apply workspace" }); + } + }, +); + +/** + * @openapi + * /workspaces/{id}: + * delete: + * summary: Delete a workspace + * description: Rejects the Last Session workspace, which is not user-deletable. + * tags: + * - Workspaces + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Workspace deleted. + * 404: + * description: Workspace not found. + */ +router.delete( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = parseWorkspaceId(req.params.id); + if (id === null) { + return res.status(400).json({ error: "Invalid workspace ID" }); + } + + try { + const deleted = await createCurrentWorkspaceRepository().delete( + userId, + id, + ); + if (!deleted) { + return res.status(404).json({ error: "Workspace not found" }); + } + res.json({ success: true }); + } catch (err) { + databaseLogger.error("Failed to delete workspace", err, { + operation: "workspace_delete_failed", + userId, + workspaceId: id, + }); + res.status(500).json({ error: "Failed to delete workspace" }); + } + }, +); + +export default router; diff --git a/src/backend/database/sync-timestamp.ts b/src/backend/database/sync-timestamp.ts new file mode 100644 index 0000000..40cfe9c --- /dev/null +++ b/src/backend/database/sync-timestamp.ts @@ -0,0 +1,35 @@ +import { sql, type SQLWrapper } from "drizzle-orm"; + +/** + * Sync cursors and stored timestamps do not share a layout. + * + * `updated_at` and `deleted_at` are TEXT columns written both by + * `default(sql`CURRENT_TIMESTAMP`)` ("2026-07-29 10:11:21") and by + * `new Date().toISOString()` ("2026-07-29T10:11:21.123Z"), while the desktop + * sync engine always sends the ISO form as `since`. Comparing those as text is + * decided at position 10, where ' ' (0x20) sorts below 'T' (0x54), so the + * answer depends on which writer produced the row rather than on when it was + * written -- and `column > :isoCursor` is false for every row stored in the + * CURRENT_TIMESTAMP form, however new it is. + * + * Both layouts share a prefix once the separator is levelled, so comparing + * "YYYY-MM-DD HH:MM:SS" on both sides is layout-independent. `replace` and + * `substr` are used rather than `datetime()` to keep the expression portable + * across engines. + */ +export const CANONICAL_TIMESTAMP_LENGTH = 19; + +export function normalizeSyncTimestamp(value: string): string { + return value.replace("T", " ").slice(0, CANONICAL_TIMESTAMP_LENGTH); +} + +/** + * `>=` rather than `>`: normalising truncates sub-second precision, so a strict + * comparison would permanently skip rows written in the same second as the + * cursor. Re-sending that boundary second costs nothing -- the sync engine only + * pushes a row when one side is strictly newer, so rows equal on both sides are + * a no-op. + */ +export function timestampAtOrAfter(column: SQLWrapper, since: string) { + return sql`substr(replace(${column}, 'T', ' '), 1, ${CANONICAL_TIMESTAMP_LENGTH}) >= ${normalizeSyncTimestamp(since)}`; +} diff --git a/src/backend/hosts/auth-manager.ts b/src/backend/hosts/auth-manager.ts index 9aceab7..a1dfe43 100644 --- a/src/backend/hosts/auth-manager.ts +++ b/src/backend/hosts/auth-manager.ts @@ -117,6 +117,24 @@ export class SSHAuthManager { return; } + // JumpCloud Protect / DUO-style push MFA: a menu choice ("Choose [1] Push, + // or [2] TOTP:") followed by an empty-answerable confirm ("Press enter to + // send Push request:"). Checked before the TOTP regex because the menu + // prompt's own text ("...or [2] TOTP:") would otherwise match it and get + // misrouted into the numeric-code flow. + const pushPromptPattern = + /choose.*push.*totp|press enter.*(push|send)|push notification|authentication by phone/i; + const isPushPrompt = promptTexts.some((p) => pushPromptPattern.test(p)); + + if (isPushPrompt) { + sshLogger.info("Push/menu MFA prompt detected", { + operation: "ssh_keyboard_interactive_push", + hostId: this.context.hostId, + }); + this.handlePasswordAuth(prompts, finish, resolvedCredentials, hostConfig); + return; + } + const totpPromptIndex = prompts.findIndex((p) => /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test( p.prompt, @@ -313,6 +331,10 @@ export class SSHAuthManager { ? passwordPromptIndex : firstUnansweredIndex; + const pushPromptPattern = + /choose.*push.*totp|press enter.*(push|send)|push notification|authentication by phone/i; + const isPushPrompt = pushPromptPattern.test(prompts[promptIndex].prompt); + this.context.keyboardInteractiveFinish = (userResponses: string[]) => { const userInput = (userResponses[0] || "").trim(); @@ -333,22 +355,25 @@ export class SSHAuthManager { clearTimeout(this.context.totpTimeout); } - this.context.totpTimeout = setTimeout(() => { - if (this.context.keyboardInteractiveFinish) { - this.context.keyboardInteractiveFinish = null; - this.context.keyboardInteractiveResponded = false; - sshLogger.warn("Password prompt timeout", { - operation: "password_timeout", - hostId: this.context.hostId, - }); - this.context.ws.send( - JSON.stringify({ - type: "error", - message: "Password verification timeout. Please reconnect.", - }), - ); - } - }, 180000); + this.context.totpTimeout = setTimeout( + () => { + if (this.context.keyboardInteractiveFinish) { + this.context.keyboardInteractiveFinish = null; + this.context.keyboardInteractiveResponded = false; + sshLogger.warn("Password prompt timeout", { + operation: "password_timeout", + hostId: this.context.hostId, + }); + this.context.ws.send( + JSON.stringify({ + type: "error", + message: "Password verification timeout. Please reconnect.", + }), + ); + } + }, + isPushPrompt ? 300000 : 180000, + ); this.sendLog("auth", "info", "Password authentication required"); @@ -356,6 +381,7 @@ export class SSHAuthManager { JSON.stringify({ type: "password_required", prompt: prompts[promptIndex].prompt, + echo: prompts[promptIndex].echo, }), ); return; diff --git a/src/backend/hosts/credential-username.ts b/src/backend/hosts/credential-username.ts index d3ca4ed..b572551 100644 --- a/src/backend/hosts/credential-username.ts +++ b/src/backend/hosts/credential-username.ts @@ -51,9 +51,30 @@ export async function expandOidcUsername( const { createCurrentUserRepository } = await import("../database/repositories/factory.js"); const user = await createCurrentUserRepository().findById(userId); - const oidcIdentifier = user?.oidcIdentifier; + let oidcIdentifier = user?.oidcIdentifier; if (!oidcIdentifier) return username; + const match = /^ldap:(\d+):(.+)$/.exec(oidcIdentifier); + if (match) { + // Make sure the SSO provider is actually LDAP, to prevent spoofing. + const { createCurrentSsoProviderRepository } = + await import("../database/repositories/factory.js"); + const claimedProviderId = Number(match[1]); + const provider = + user?.ssoProviderId != null + ? await createCurrentSsoProviderRepository().findById( + user.ssoProviderId, + ) + : null; + + if ( + provider?.type === "ldap" && + user?.ssoProviderId === claimedProviderId + ) { + oidcIdentifier = match[2]; + } + } + return username.replace(/\$oidc\.preferred_username/g, oidcIdentifier); } catch { return username; diff --git a/src/backend/hosts/docker/console.ts b/src/backend/hosts/docker/console.ts index 46a3c9b..d1088e6 100644 --- a/src/backend/hosts/docker/console.ts +++ b/src/backend/hosts/docker/console.ts @@ -1,3 +1,5 @@ +import { getErrorMessage } from "../../utils/error-message.js"; +import { StringDecoder } from "string_decoder"; import { Client as SSHClient } from "ssh2"; import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js"; import { WebSocketServer, WebSocket } from "ws"; @@ -12,6 +14,11 @@ import { type ContainerRuntime, } from "./container-runtime.js"; import { resolveSshConnectConfigHost } from "../ssh-dns.js"; +import { + hostAddressMismatch, + HOST_ADDRESS_MISMATCH_MESSAGE, + HOST_NOT_ON_THIS_SERVER_MESSAGE, +} from "../terminal/host-identity.js"; const sshLogger = systemLogger; @@ -32,6 +39,12 @@ const wss = new WebSocketServer({ port: 30009, }); +wss.on("error", (error) => { + sshLogger.error("Docker console WebSocket server error", error, { + operation: "wss_error", + }); +}); + async function detectShell( session: SSHSession, containerId: string, @@ -133,8 +146,7 @@ async function createJumpHostChain( resolvedCredentials = { password: credential.password as string | undefined, sshKey: (credential.key || credential.privateKey) as - | string - | undefined, + string | undefined, keyPassword: credential.keyPassword as string | undefined, authType: credential.authType as string | undefined, }; @@ -214,8 +226,7 @@ async function createJumpHostChain( const result = await applyAgentAuth( config, jumpHost.terminalConfig as unknown as - | Record - | undefined, + Record | undefined, ); if ("error" in result) { throw new Error(result.error); @@ -383,14 +394,49 @@ wss.on("connection", async (ws: WebSocket, req) => { try { // Resolve host with credentials server-side - const { resolveHostById } = await import("../host-resolver.js"); - const resolvedHost = await resolveHostById(hostId, userId); + const { resolveHostById, resolveHostBySyncId } = + await import("../host-resolver.js"); + // syncId names the host on both sides of a sync pair; the numeric + // id only names it in the database the client is displaying. + const hostSyncId = hostConfig?.syncId; + const resolvedHost = hostSyncId + ? await resolveHostBySyncId(hostSyncId, userId) + : await resolveHostById(hostId, userId); if (!resolvedHost) { ws.send( JSON.stringify({ type: "error", - message: "Host not found", + message: hostSyncId + ? HOST_NOT_ON_THIS_SERVER_MESSAGE + : "Host not found", + }), + ); + return; + } + + // The connection below dials resolvedHost.ip outright, so if this + // server has a different machine under the id the client sent, the + // console opens on that machine's Docker daemon instead. + if ( + !hostSyncId && + hostAddressMismatch(hostConfig?.ip, resolvedHost.ip) + ) { + sshLogger.error( + "Refusing Docker console: host id resolves to a different address here", + undefined, + { + operation: "docker_console_host_id_mismatch", + hostId, + userId, + clientIp: hostConfig?.ip, + resolvedIp: resolvedHost.ip, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: HOST_ADDRESS_MISMATCH_MESSAGE, }), ); return; @@ -436,8 +482,7 @@ wss.on("connection", async (ws: WebSocket, req) => { const result = await applyAgentAuth( config, resolvedHost.terminalConfig as unknown as - | Record - | undefined, + Record | undefined, ); if ("error" in result) { ws.send( @@ -598,12 +643,19 @@ wss.on("connection", async (ws: WebSocket, req) => { containerId, }); + // Buffers incomplete multi-byte UTF-8 sequences across chunk + // boundaries so box-drawing/special characters don't get + // corrupted when a character is split across TCP packets. + const decoder = new StringDecoder("utf-8"); + stream.on("data", (data: Buffer) => { if (ws.readyState === WebSocket.OPEN) { + const text = decoder.write(data); + if (!text) return; ws.send( JSON.stringify({ type: "output", - data: data.toString("utf8"), + data: text, }), ); } @@ -666,10 +718,10 @@ wss.on("connection", async (ws: WebSocket, req) => { ws.send( JSON.stringify({ type: "error", - message: - error instanceof Error - ? error.message - : "Failed to connect to container", + message: getErrorMessage( + error, + "Failed to connect to container", + ), }), ); } @@ -731,7 +783,7 @@ wss.on("connection", async (ws: WebSocket, req) => { ws.send( JSON.stringify({ type: "error", - message: error instanceof Error ? error.message : "An error occurred", + message: getErrorMessage(error, "An error occurred"), }), ); } diff --git a/src/backend/hosts/docker/container-routes.ts b/src/backend/hosts/docker/container-routes.ts index 75d5f92..4bc344f 100644 --- a/src/backend/hosts/docker/container-routes.ts +++ b/src/backend/hosts/docker/container-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type express from "express"; import { logger } from "../../utils/logger.js"; import { @@ -151,8 +152,7 @@ export function registerDockerContainerRoutes( }); res.status(500).json({ - error: - error instanceof Error ? error.message : "Failed to list containers", + error: getErrorMessage(error, "Failed to list containers"), }); } }); @@ -232,7 +232,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ error: "Container not found", @@ -329,7 +329,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, @@ -429,7 +429,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, @@ -529,7 +529,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, @@ -629,7 +629,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, @@ -729,7 +729,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, @@ -838,7 +838,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, @@ -982,7 +982,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, @@ -1104,7 +1104,7 @@ export function registerDockerContainerRoutes( } catch (error) { session.activeOperations--; - const errorMsg = error instanceof Error ? error.message : ""; + const errorMsg = getErrorMessage(error, ""); if (errorMsg.includes("No such container")) { return res.status(404).json({ success: false, diff --git a/src/backend/hosts/docker/index.ts b/src/backend/hosts/docker/index.ts index c6f9e3a..9619825 100644 --- a/src/backend/hosts/docker/index.ts +++ b/src/backend/hosts/docker/index.ts @@ -1,6 +1,7 @@ import express from "express"; import cookieParser from "cookie-parser"; import { createCorsMiddleware } from "../../utils/cors-config.js"; +import { createCompressionMiddleware } from "../../utils/compression-config.js"; import { logger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { registerDockerContainerRoutes } from "./container-routes.js"; @@ -20,6 +21,7 @@ const sshLogger = logger; const app = express(); +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"])); app.use(cookieParser()); diff --git a/src/backend/hosts/docker/routes.ts b/src/backend/hosts/docker/routes.ts index 2cebf27..78149f8 100644 --- a/src/backend/hosts/docker/routes.ts +++ b/src/backend/hosts/docker/routes.ts @@ -1,22 +1,28 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import express from "express"; import axios from "axios"; import { Client as SSHClient } from "ssh2"; import { logger } from "../../utils/logger.js"; import { - createCurrentCredentialRepository, - createCurrentHostRepository, - createCurrentHostResolutionRepository, -} from "../../database/repositories/factory.js"; + logAudit, + getAuditUsername, + getRequestMeta, +} from "../../utils/audit-logger.js"; +import { createCurrentHostRepository } from "../../database/repositories/factory.js"; import { createJumpHostChain } from "../jump-host-chain.js"; +import { resolveHostById } from "../host-resolver.js"; import { createConnectionLog } from "../connection-log.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import type { AuthenticatedRequest } from "../../../types/index.js"; +import { + type AuthenticatedRequest, + type ProxyNode, + type SSHHost, +} from "../../../types/index.js"; import { createSocks5Connection, type SOCKS5Config, } from "../../utils/socks5-helper.js"; -import type { SSHHost, ProxyNode } from "../../../types/index.js"; import type { LogEntry, ConnectionStage, @@ -158,13 +164,8 @@ export function registerDockerSshRoutes(app: express.Express): void { ); try { - const hostRecord = - await createCurrentHostResolutionRepository().findHostById( - hostId, - userId, - ); - - if (!hostRecord) { + const resolvedHost = await resolveHostById(hostId, userId); + if (!resolvedHost) { connectionLogs.push( createConnectionLog("error", "docker_connecting", "Host not found"), ); @@ -173,36 +174,7 @@ export function registerDockerSshRoutes(app: express.Express): void { .json({ error: "Host not found", connectionLogs }); } - const host = hostRecord as unknown as SSHHost; - - if (host.userId !== userId) { - const { PermissionManager } = - await import("../../utils/permission-manager.js"); - const permissionManager = PermissionManager.getInstance(); - const accessInfo = await permissionManager.canAccessHost( - userId, - hostId, - "connect", - ); - - if (!accessInfo.hasAccess) { - sshLogger.warn("User does not have access to host", { - operation: "docker_connect", - hostId, - userId, - }); - connectionLogs.push( - createConnectionLog( - "error", - "docker_connecting", - "Access denied to host", - ), - ); - return res - .status(403) - .json({ error: "Access denied", connectionLogs }); - } - } + const host = resolvedHost as SSHHost; if (typeof host.jumpHosts === "string" && host.jumpHosts) { try { host.jumpHosts = JSON.parse(host.jumpHosts); @@ -266,7 +238,7 @@ export function registerDockerSshRoutes(app: express.Express): void { delete pendingTOTPSessions[sessionId]; } - let resolvedCredentials: { + const resolvedCredentials: { password?: string; sshKey?: string; keyPassword?: string; @@ -280,6 +252,7 @@ export function registerDockerSshRoutes(app: express.Express): void { if (userProvidedPassword) { resolvedCredentials.password = userProvidedPassword; + resolvedCredentials.authType = "password"; } if (userProvidedSshKey) { resolvedCredentials.sshKey = userProvidedSshKey; @@ -289,55 +262,6 @@ export function registerDockerSshRoutes(app: express.Express): void { resolvedCredentials.keyPassword = userProvidedKeyPassword; } - if (host.credentialId) { - const ownerId = host.userId; - - if (userId !== ownerId) { - try { - const { SharedHostSecretsManager } = - await import("../../utils/shared-host-secrets-manager.js"); - const sharedCred = - await SharedHostSecretsManager.getInstance().getSecretForUser( - host.id, - userId, - "ssh", - ); - - if (sharedCred) { - resolvedCredentials = { - password: sharedCred.password, - sshKey: sharedCred.key, - keyPassword: sharedCred.keyPassword, - authType: sharedCred.authType, - }; - } - } catch (error) { - sshLogger.error("Failed to resolve shared credential", error, { - operation: "docker_connect", - hostId, - userId, - }); - } - } else { - const credential = - await createCurrentCredentialRepository().findDecryptedByIdForUser( - userId, - host.credentialId as number, - ); - - if (credential) { - resolvedCredentials = { - password: credential.password as string | undefined, - sshKey: (credential.key || credential.privateKey) as - | string - | undefined, - keyPassword: credential.keyPassword as string | undefined, - authType: credential.authType as string | undefined, - }; - } - } - } - const client = new SSHClient(); const config: Record = { @@ -417,16 +341,13 @@ export function registerDockerSshRoutes(app: express.Express): void { operation: "docker_connect", sessionId, hostId, - error: - opksshError instanceof Error - ? opksshError.message - : "Unknown error", + error: getErrorMessage(opksshError), }); connectionLogs.push( createConnectionLog( "error", "docker_auth", - `OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`, + `OPKSSH authentication failed: ${getErrorMessage(opksshError)}`, ), ); return res.status(500).json({ @@ -447,10 +368,7 @@ export function registerDockerSshRoutes(app: express.Express): void { config.passphrase = resolvedCredentials.keyPassword; } } catch (error) { - const message = - error instanceof Error - ? error.message - : "Invalid private key format"; + const message = getErrorMessage(error, "Invalid private key format"); sshLogger.error("SSH key processing error", error, { operation: "docker_connect", sessionId, @@ -590,6 +508,20 @@ export function registerDockerSshRoutes(app: express.Express): void { } }); + void (async () => { + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "docker_connect", + resourceType: "host", + resourceId: hostId ? String(hostId) : undefined, + ipAddress, + userAgent, + success: true, + }); + })(); + res.json({ success: true, message: "SSH connection established", @@ -993,7 +925,6 @@ export function registerDockerSshRoutes(app: express.Express): void { const jumpClient = await createJumpHostChain( host.jumpHosts as Array<{ hostId: number }>, userId, - proxyConfig, ); if (!jumpClient) { @@ -1055,17 +986,14 @@ export function registerDockerSshRoutes(app: express.Express): void { createConnectionLog( "error", "jump", - `Jump host connection failed: ${jumpError instanceof Error ? jumpError.message : "Unknown error"}`, + `Jump host connection failed: ${getErrorMessage(jumpError)}`, ), ); if (!responseSent) { responseSent = true; return res.status(500).json({ error: - "Jump host connection failed: " + - (jumpError instanceof Error - ? jumpError.message - : "Unknown error"), + "Jump host connection failed: " + getErrorMessage(jumpError), connectionLogs, }); } @@ -1095,17 +1023,13 @@ export function registerDockerSshRoutes(app: express.Express): void { createConnectionLog( "error", "proxy", - `Proxy connection failed: ${proxyError instanceof Error ? proxyError.message : "Unknown error"}`, + `Proxy connection failed: ${getErrorMessage(proxyError)}`, ), ); if (!responseSent) { responseSent = true; return res.status(500).json({ - error: - "Proxy connection failed: " + - (proxyError instanceof Error - ? proxyError.message - : "Unknown error"), + error: "Proxy connection failed: " + getErrorMessage(proxyError), connectionLogs, }); } @@ -1126,12 +1050,12 @@ export function registerDockerSshRoutes(app: express.Express): void { createConnectionLog( "error", "docker_connecting", - `Connection error: ${error instanceof Error ? error.message : "Unknown error"}`, + `Connection error: ${getErrorMessage(error)}`, ), ); res.status(500).json({ success: false, - message: error instanceof Error ? error.message : "Unknown error", + message: getErrorMessage(error), connectionLogs, }); } @@ -1331,7 +1255,7 @@ export function registerDockerSshRoutes(app: express.Express): void { operation: "activity_log_error", userId: session.userId, hostId: session.hostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } })(); @@ -1511,7 +1435,7 @@ export function registerDockerSshRoutes(app: express.Express): void { operation: "activity_log_error", userId: session.userId, hostId: session.hostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } })(); @@ -1720,8 +1644,7 @@ export function registerDockerSshRoutes(app: express.Express): void { }); } catch (daemonError) { session.activeOperations--; - const errorMsg = - daemonError instanceof Error ? daemonError.message : ""; + const errorMsg = getErrorMessage(daemonError, ""); if (errorMsg.includes("Cannot connect to the Docker daemon")) { return res.json({ @@ -1769,7 +1692,7 @@ export function registerDockerSshRoutes(app: express.Express): void { res.status(500).json({ available: false, - error: error instanceof Error ? error.message : "Validation failed", + error: getErrorMessage(error, "Validation failed"), }); } }); diff --git a/src/backend/hosts/file-manager/ca-cert-auth.ts b/src/backend/hosts/file-manager/ca-cert-auth.ts new file mode 100644 index 0000000..552b4a8 --- /dev/null +++ b/src/backend/hosts/file-manager/ca-cert-auth.ts @@ -0,0 +1,51 @@ +import { getErrorMessage } from "../../utils/error-message.js"; +import type { Client as SSHClient, ConnectConfig } from "ssh2"; +import { fileLogger } from "../../utils/logger.js"; + +/** + * Attaches a user-managed CA-signed certificate to an SFTP connection, if the + * host carries one. + * + * The terminal has always done this. The file manager never did: it imports + * and calls `setupOPKSSHCertAuth`, but `setupCACertAuth` โ€” the sibling helper + * for user-managed `-cert.pub` files โ€” had no call site here at all. A host + * whose key is paired with a CA-signed certificate therefore authenticated in + * a terminal and failed in the file manager, while OPKSSH certificates worked + * in both. That asymmetry is the whole bug. + * + * `cert_public_key` is a column on `ssh_data` but is not part of the shared + * `Host` type, so callers pass whichever record they hold and it is read off + * structurally โ€” the same way the terminal reads it. + * + * A certificate that cannot be applied is logged and skipped rather than + * failing the connection: the private key alone may still be accepted, which + * is exactly what happened before any of this was wired up. + */ +export async function applyCACertIfPresent( + config: Record, + client: SSHClient, + privateKey: Buffer | string, + source: { certPublicKey?: string | null }, + username: string, + passphrase?: string, +): Promise { + const certPublicKey = source.certPublicKey; + if (!certPublicKey || !certPublicKey.trim()) return; + + try { + const { setupCACertAuth } = await import("../opkssh-cert-auth.js"); + await setupCACertAuth( + config as ConnectConfig, + client, + privateKey, + certPublicKey, + username, + passphrase, + ); + } catch (certError) { + fileLogger.warn("CA certificate setup failed, continuing with key only", { + operation: "sftp_ca_cert_auth_failed", + error: getErrorMessage(certError), + }); + } +} diff --git a/src/backend/hosts/file-manager/direct-transfer-routing.ts b/src/backend/hosts/file-manager/direct-transfer-routing.ts new file mode 100644 index 0000000..e490d34 --- /dev/null +++ b/src/backend/hosts/file-manager/direct-transfer-routing.ts @@ -0,0 +1,72 @@ +export interface DirectTransferEndpoint { + host: string; + port: number; + username: string; +} + +export const DIRECT_TRANSFER_MIN_IMPROVEMENT = 0.2; +export const DIRECT_TRANSFER_MIN_BYTES = 32 * 1024 * 1024; + +export function shouldBenchmarkDirectTransfer(totalBytes: number): boolean { + return totalBytes >= DIRECT_TRANSFER_MIN_BYTES; +} + +export function quoteShell(value: string): string { + return `'${value.replace(/'/g, `'"'"'`)}'`; +} + +function formatHost(host: string): string { + return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; +} + +export function buildDirectSshCommand( + endpoint: DirectTransferEndpoint, +): string { + const target = `${endpoint.username}@${formatHost(endpoint.host)}`; + return [ + "ssh", + "-o BatchMode=yes", + "-o ConnectTimeout=5", + "-o StrictHostKeyChecking=yes", + `-p ${endpoint.port}`, + quoteShell(target), + ].join(" "); +} + +export function buildDirectProbeCommand( + endpoint: DirectTransferEndpoint, +): string { + return `${buildDirectSshCommand(endpoint)} ${quoteShell("command -v rsync >/dev/null")}`; +} + +export function buildDirectRsyncCommand( + endpoint: DirectTransferEndpoint, + sourcePaths: string[], + destPath: string, + destIsDirectory: boolean, +): string { + const sshTransport = buildDirectSshCommand(endpoint).replace(/ '[^']+'$/, ""); + const targetHost = `${endpoint.username}@${formatHost(endpoint.host)}`; + const targetPath = destIsDirectory + ? `${destPath.replace(/\/+$/, "") || "/"}/` + : destPath; + const sources = sourcePaths.map(quoteShell).join(" "); + const target = quoteShell(`${targetHost}:${targetPath}`); + + return [ + "rsync -a --partial --append-verify --protect-args --info=progress2", + `-e ${quoteShell(sshTransport)}`, + "--", + sources, + target, + ].join(" "); +} + +export function shouldUseDirectTransfer( + directMs: number, + relayMs: number, + minImprovement = DIRECT_TRANSFER_MIN_IMPROVEMENT, +): boolean { + if (directMs <= 0 || relayMs <= 0) return false; + return directMs <= relayMs * (1 - minImprovement); +} diff --git a/src/backend/hosts/file-manager/index.ts b/src/backend/hosts/file-manager/index.ts index 5f378a6..03d10a3 100644 --- a/src/backend/hosts/file-manager/index.ts +++ b/src/backend/hosts/file-manager/index.ts @@ -1,5 +1,12 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import express from "express"; +import { + logAudit, + getAuditUsername, + getRequestMeta, +} from "../../utils/audit-logger.js"; import { createCorsMiddleware } from "../../utils/cors-config.js"; +import { createCompressionMiddleware } from "../../utils/compression-config.js"; import cookieParser from "cookie-parser"; import axios from "axios"; import { Client as SSHClient } from "ssh2"; @@ -7,7 +14,11 @@ import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js"; import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js"; import { fileLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import type { AuthenticatedRequest, ProxyNode } from "../../../types/index.js"; +import { + type AuthenticatedRequest, + type ProxyNode, + type SSHHost, +} from "../../../types/index.js"; import { createSocks5Connection, type SOCKS5Config, @@ -18,7 +29,6 @@ import type { } from "../../../types/connection-log.js"; import { SSHHostKeyVerifier } from "../host-key-verifier.js"; import { resolveHostById } from "../host-resolver.js"; -import type { SSHHost } from "../../../types/index.js"; import { startHostTransfer, getTransferStatus, @@ -44,12 +54,68 @@ import { import { registerFileListingRoutes } from "./list-routes.js"; import { registerFileOperationRoutes } from "./operation-routes.js"; import { resolveSshConnectConfigHost } from "../ssh-dns.js"; +import { resolveSshKeepalive } from "../ssh-keepalive.js"; +import { + hostAddressMismatch, + HostAddressMismatchError, + HostNotOnThisServerError, +} from "../terminal/host-identity.js"; import { registerFileDownloadRoutes } from "./download-routes.js"; import { registerFileActionRoutes } from "./action-routes.js"; import { applyAgentAuth } from "../terminal-auth-helpers.js"; +import { applyCACertIfPresent } from "./ca-cert-auth.js"; + +/** + * The host id came from whichever database the client is displaying. If this + * server has a different machine under that id โ€” desktop and sync server + * autoincrement sequences drift apart โ€” then the address, the credentials and + * the jump hosts resolved from it all belong to that other machine, and the + * user would browse, edit and delete its files believing they are on the host + * they picked. + */ +function assertResolvedHost( + clientIp: unknown, + hostSyncId: string | null | undefined, + resolvedHost: { ip?: string } | null | undefined, + hostId: number, + userId: string, +): void { + // Named by sync identity: it either exists here or it does not. Falling back + // to the numeric id is what picks the wrong machine. + if (hostSyncId) { + if (resolvedHost) return; + fileLogger.error( + "Refusing SFTP connection: host is not known to this server", + undefined, + { + operation: "file_manager_host_sync_id_unknown", + hostId, + userId, + }, + ); + throw new HostNotOnThisServerError(); + } + + // Older clients send only the numeric id, which means a different host here. + if (!hostAddressMismatch(clientIp, resolvedHost?.ip)) return; + + fileLogger.error( + "Refusing SFTP connection: host id resolves to a different address here", + undefined, + { + operation: "file_manager_host_id_mismatch", + hostId, + userId, + clientIp, + resolvedIp: resolvedHost?.ip, + }, + ); + throw new HostAddressMismatchError(); +} const app = express(); +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"])); app.use(cookieParser()); app.use(express.json({ limit: "1gb" })); @@ -206,6 +272,15 @@ async function buildDedicatedTransferConnectConfig( .replace(/\r/g, "\n"); config.privateKey = Buffer.from(cleanKey, "utf8"); if (host.keyPassword) config.passphrase = host.keyPassword; + + await applyCACertIfPresent( + config, + client, + config.privateKey as Buffer, + host as { certPublicKey?: string | null }, + username, + host.keyPassword, + ); } else if (authType === "password") { if (!host.password) { throw new Error("Password required for transfer connection"); @@ -289,11 +364,7 @@ async function startDedicatedTransferConnect( const hasJumpHosts = jumpHosts && jumpHosts.length > 0; if (hasJumpHosts) { - const jumpClient = await createJumpHostChain( - jumpHosts, - userId, - proxyConfig, - ); + const jumpClient = await createJumpHostChain(jumpHosts, userId); if (!jumpClient) { throw new Error("Failed to connect through jump hosts for transfer"); } @@ -367,8 +438,12 @@ async function openDedicatedTransferSession( throw new Error("Host not found for transfer connection"); } - if (sshSessions[dedicatedSessionId]?.isConnected) { - closeDedicatedTransferSession(dedicatedSessionId); + const existingSession = sshSessions[dedicatedSessionId]; + if ( + existingSession?.isConnected && + verifySessionOwnership(existingSession, userId) + ) { + return existingSession; } const client = new SSHClient(); @@ -638,6 +713,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { const { sessionId, hostId, + syncId: hostSyncId, ip, port, username, @@ -733,6 +809,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { keyPassword, authType, sudoPassword: undefined as string | undefined, + certPublicKey: undefined as string | undefined, }; let hostKeepaliveInterval: number | undefined; let hostKeepaliveCountMax: number | undefined; @@ -750,8 +827,12 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { let resolvedSocks5ProxyChain = socks5ProxyChain; if (hostId && userId && !password && !sshKey) { try { - const { resolveHostById } = await import("../host-resolver.js"); - const resolvedHost = await resolveHostById(hostId, userId); + const { resolveHostById, resolveHostBySyncId } = + await import("../host-resolver.js"); + const resolvedHost = hostSyncId + ? await resolveHostBySyncId(hostSyncId, userId) + : await resolveHostById(hostId, userId); + assertResolvedHost(ip, hostSyncId, resolvedHost, hostId, userId); if (resolvedHost) { resolvedIp = resolvedHost.ip; resolvedPort = resolvedHost.port; @@ -762,10 +843,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { keyPassword: resolvedHost.keyPassword, authType: resolvedHost.authType, sudoPassword: resolvedHost.sudoPassword as string | undefined, + certPublicKey: (resolvedHost as { certPublicKey?: string }) + .certPublicKey, }; resolvedTerminalConfig = resolvedHost.terminalConfig as unknown as - | Record - | undefined; + Record | undefined; hostKeepaliveInterval = resolvedHost.terminalConfig?.keepaliveInterval; hostKeepaliveCountMax = resolvedHost.terminalConfig?.keepaliveCountMax; resolvedScpLegacy = resolvedHost.scpLegacy ?? false; @@ -800,17 +882,26 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { ); } } catch (error) { + if ( + error instanceof HostAddressMismatchError || + error instanceof HostNotOnThisServerError + ) + throw error; fileLogger.warn(`Failed to resolve host credentials for ${hostId}`, { operation: "ssh_credentials", hostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } else if (credentialId && hostId && userId) { // Legacy: credential resolution from credentialId try { - const { resolveHostById } = await import("../host-resolver.js"); - const resolvedHost = await resolveHostById(hostId, userId); + const { resolveHostById, resolveHostBySyncId } = + await import("../host-resolver.js"); + const resolvedHost = hostSyncId + ? await resolveHostBySyncId(hostSyncId, userId) + : await resolveHostById(hostId, userId); + assertResolvedHost(ip, hostSyncId, resolvedHost, hostId, userId); if (resolvedHost) { resolvedIp = resolvedHost.ip; resolvedPort = resolvedHost.port; @@ -821,10 +912,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { keyPassword: resolvedHost.keyPassword, authType: resolvedHost.authType, sudoPassword: resolvedHost.sudoPassword as string | undefined, + certPublicKey: (resolvedHost as { certPublicKey?: string }) + .certPublicKey, }; resolvedTerminalConfig = resolvedHost.terminalConfig as unknown as - | Record - | undefined; + Record | undefined; hostKeepaliveInterval = resolvedHost.terminalConfig?.keepaliveInterval; hostKeepaliveCountMax = resolvedHost.terminalConfig?.keepaliveCountMax; resolvedScpLegacy = resolvedHost.scpLegacy ?? false; @@ -859,29 +951,33 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { ); } } catch (error) { + if ( + error instanceof HostAddressMismatchError || + error instanceof HostNotOnThisServerError + ) + throw error; fileLogger.warn(`Failed to resolve credentials for host ${hostId}`, { operation: "ssh_credentials", hostId, credentialId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } const preloadedHostData = await SSHHostKeyVerifier.preloadHostData(hostId); + const keepalive = resolveSshKeepalive( + hostKeepaliveInterval, + hostKeepaliveCountMax, + 60000, + 5, + ); const config: Record = { host: resolvedIp?.replace(/^\[|\]$/g, "") || resolvedIp, port: resolvedPort, username: resolvedUsername, tryKeyboard: true, - keepaliveInterval: - typeof hostKeepaliveInterval === "number" - ? Math.max(5000, hostKeepaliveInterval * 1000) - : 60000, - keepaliveCountMax: - typeof hostKeepaliveCountMax === "number" - ? Math.max(1, hostKeepaliveCountMax) - : 5, + ...keepalive, readyTimeout: 60000, tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, @@ -955,11 +1051,23 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { if (resolvedCredentials.keyPassword) config.passphrase = resolvedCredentials.keyPassword; + + await applyCACertIfPresent( + config, + client, + config.privateKey as Buffer, + resolvedCredentials, + resolvedUsername, + resolvedCredentials.keyPassword, + ); + connectionLogs.push( createConnectionLog( "info", "sftp_auth", - "Using SSH key authentication", + resolvedCredentials.certPublicKey?.trim() + ? "Using SSH key authentication with CA certificate" + : "Using SSH key authentication", ), ); } catch (keyError) { @@ -1039,14 +1147,13 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { operation: "file_connect", sessionId, hostId, - error: - opksshError instanceof Error ? opksshError.message : "Unknown error", + error: getErrorMessage(opksshError), }); connectionLogs.push( createConnectionLog( "error", "sftp_auth", - `OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`, + `OPKSSH authentication failed: ${getErrorMessage(opksshError)}`, ), ); return res.status(500).json({ @@ -1169,6 +1276,24 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { scpLegacy: resolvedScpLegacy, }; scheduleSessionCleanup(sessionId); + + if (userId) { + const { ipAddress, userAgent } = getRequestMeta(req); + void (async () => { + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "file_manager_connect", + resourceType: "host", + resourceId: hostId ? String(hostId) : undefined, + resourceName: `${username}@${ip}:${port}`, + ipAddress, + userAgent, + success: true, + }); + })(); + } + res.json({ status: "success", message: "SSH connection established", @@ -1208,7 +1333,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { operation: "activity_log_error", userId, hostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } })(); @@ -1616,11 +1741,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { `Connecting via ${resolvedJumpHosts.length} jump host(s)`, ), ); - const jumpClient = await createJumpHostChain( - resolvedJumpHosts, - userId, - proxyConfig, - ); + const jumpClient = await createJumpHostChain(resolvedJumpHosts, userId); if (!jumpClient) { fileLogger.error("Failed to establish jump host chain", { @@ -1707,7 +1828,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { createConnectionLog( "error", "jump", - `Jump host error: ${error instanceof Error ? error.message : "Unknown error"}`, + `Jump host error: ${getErrorMessage(error)}`, ), ); return res.status(500).json({ @@ -1747,13 +1868,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { createConnectionLog( "error", "proxy", - `Proxy connection failed: ${proxyError instanceof Error ? proxyError.message : "Unknown error"}`, + `Proxy connection failed: ${getErrorMessage(proxyError)}`, ), ); return res.status(500).json({ - error: - "Proxy connection failed: " + - (proxyError instanceof Error ? proxyError.message : "Unknown error"), + error: "Proxy connection failed: " + getErrorMessage(proxyError), connectionLogs, }); } @@ -1904,7 +2023,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => { operation: "activity_log_error", userId: session.userId, hostId: session.hostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } })(); @@ -2102,7 +2221,7 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => { operation: "activity_log_error", userId: session.userId, hostId: session.hostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } })(); @@ -2816,7 +2935,7 @@ app.post("/ssh/file_manager/ssh/transferMethodPreview", async (req, res) => { sourcePaths, }); res.status(500).json({ - error: err instanceof Error ? err.message : "Failed to preview method", + error: getErrorMessage(err, "Failed to preview method"), }); } }); @@ -2867,7 +2986,7 @@ app.post("/ssh/file_manager/ssh/transferToHost", async (req, res) => { const rawParallel = Number(parallelSegmentCountRaw); const parallelSegmentCount = Number.isFinite(rawParallel) ? Math.max(1, Math.min(8, Math.floor(rawParallel))) - : 2; + : undefined; const { transferId } = startHostTransfer(hostTransferDeps, { sourceSessionId, @@ -2955,8 +3074,7 @@ app.post( ); res.json(result); } catch (err) { - const message = - err instanceof Error ? err.message : "Failed to clean up transfer"; + const message = getErrorMessage(err, "Failed to clean up transfer"); const status = message === "Transfer not found" ? 404 : 400; res.status(status).json({ error: message }); } diff --git a/src/backend/hosts/file-manager/list-routes.ts b/src/backend/hosts/file-manager/list-routes.ts index a4b1a34..40e7072 100644 --- a/src/backend/hosts/file-manager/list-routes.ts +++ b/src/backend/hosts/file-manager/list-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { Express } from "express"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { fileLogger } from "../../utils/logger.js"; @@ -194,8 +195,7 @@ export function registerFileListingRoutes( tryFallbackMethod(); }); } catch (sftpErr: unknown) { - const errMsg = - sftpErr instanceof Error ? sftpErr.message : "Unknown error"; + const errMsg = getErrorMessage(sftpErr); fileLogger.warn(`SFTP connection error, trying fallback: ${errMsg}`); tryFallbackMethod(); } @@ -325,8 +325,7 @@ export function registerFileListingRoutes( ); } catch (execErr: unknown) { sshConn.activeOperations--; - const errMsg = - execErr instanceof Error ? execErr.message : "Unknown error"; + const errMsg = getErrorMessage(execErr); fileLogger.error(`Fallback listFiles exec failed: ${errMsg}`); if (!res.headersSent) { return res.status(500).json({ error: errMsg }); @@ -455,8 +454,7 @@ export function registerFileListingRoutes( }); } catch (execErr: unknown) { sshConn.activeOperations--; - const errMsg = - execErr instanceof Error ? execErr.message : "Unknown error"; + const errMsg = getErrorMessage(execErr); fileLogger.error(`Sudo listFiles exec failed: ${errMsg}`); if (!res.headersSent) { return res.status(500).json({ error: errMsg }); diff --git a/src/backend/hosts/file-manager/operation-routes.ts b/src/backend/hosts/file-manager/operation-routes.ts index 4a2e497..952dfc8 100644 --- a/src/backend/hosts/file-manager/operation-routes.ts +++ b/src/backend/hosts/file-manager/operation-routes.ts @@ -1,8 +1,25 @@ import type { Express } from "express"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { fileLogger } from "../../utils/logger.js"; -import { execChannel, execWithSudo, type SSHSession } from "./session.js"; +import { + execChannel, + execWithSudo, + getSessionSftp, + type SSHSession, +} from "./session.js"; import { buildDeleteCommand } from "./operation-commands.js"; +import { + emptyTrash, + listTrash, + moveToTrash, + permanentlyDeleteTrashItem, + restoreTrashItem, +} from "./trash-service.js"; +import { + createCurrentSettingsRepository, + getCurrentSettingValue, +} from "../../database/repositories/factory.js"; +import { PermissionManager } from "../../utils/permission-manager.js"; type FileOperationRoutesDeps = { sshSessions: Record; @@ -13,6 +30,128 @@ export function registerFileOperationRoutes( app: Express, { sshSessions, verifySessionOwnership }: FileOperationRoutesDeps, ): void { + const permissionManager = PermissionManager.getInstance(); + const getTrashRetentionDays = () => { + try { + const value = Number( + getCurrentSettingValue("file_manager_trash_retention_days"), + ); + return Number.isInteger(value) && value >= 1 && value <= 3650 ? value : 7; + } catch { + return 7; + } + }; + + async function ownedSession( + req: AuthenticatedRequest, + res: import("express").Response, + ) { + const sessionId = String(req.body?.sessionId ?? req.query?.sessionId ?? ""); + const session = sshSessions[sessionId]; + if (!sessionId || !session?.isConnected) { + res.status(400).json({ error: "SSH connection not established" }); + return null; + } + if (!verifySessionOwnership(session, req.userId)) { + res.status(403).json({ error: "Session access denied" }); + return null; + } + session.lastActive = Date.now(); + return session; + } + + app.get("/ssh/file_manager/ssh/trash", async (req, res) => { + const session = await ownedSession( + req as unknown as AuthenticatedRequest, + res, + ); + if (!session) return; + try { + res.json({ + items: await listTrash( + await getSessionSftp(session), + getTrashRetentionDays(), + ), + retentionDays: getTrashRetentionDays(), + canManageRetention: await permissionManager.isAdmin( + (req as AuthenticatedRequest).userId, + ), + }); + } catch (error) { + fileLogger.error("Failed to list trash", error); + res.status(500).json({ error: (error as Error).message }); + } + }); + + app.post("/ssh/file_manager/ssh/trash/:id/restore", async (req, res) => { + const session = await ownedSession( + req as unknown as AuthenticatedRequest, + res, + ); + if (!session) return; + try { + res.json({ + item: await restoreTrashItem( + await getSessionSftp(session), + req.params.id, + ), + }); + } catch (error) { + const message = (error as Error).message; + res + .status(message.includes("already exists") ? 409 : 500) + .json({ error: message }); + } + }); + + app.delete("/ssh/file_manager/ssh/trash/:id", async (req, res) => { + const session = await ownedSession( + req as unknown as AuthenticatedRequest, + res, + ); + if (!session) return; + try { + await permanentlyDeleteTrashItem( + await getSessionSftp(session), + req.params.id, + ); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } + }); + + app.delete("/ssh/file_manager/ssh/trash", async (req, res) => { + const session = await ownedSession(req as AuthenticatedRequest, res); + if (!session) return; + try { + res.json({ deleted: await emptyTrash(await getSessionSftp(session)) }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } + }); + + app.put("/ssh/file_manager/ssh/trash-retention", async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + if (!(await permissionManager.isAdmin(userId))) { + return res.status(403).json({ error: "Admin access required" }); + } + const retentionDays = Number(req.body?.retentionDays); + if ( + !Number.isInteger(retentionDays) || + retentionDays < 1 || + retentionDays > 3650 + ) { + return res + .status(400) + .json({ error: "Retention must be between 1 and 3650 days" }); + } + await createCurrentSettingsRepository().upsert( + "file_manager_trash_retention_days", + String(retentionDays), + ); + return res.json({ retentionDays }); + }); /** * @openapi * /ssh/file_manager/ssh/createFile: @@ -342,7 +481,7 @@ export function registerFileOperationRoutes( * description: Failed to delete item. */ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { - const { sessionId, path: itemPath, isDirectory } = req.body; + const { sessionId, path: itemPath, isDirectory, permanent } = req.body; const sshConn = sshSessions[sessionId]; const userId = (req as AuthenticatedRequest).userId; @@ -371,6 +510,37 @@ export function registerFileOperationRoutes( }); sshConn.lastActive = Date.now(); + if (!permanent) { + try { + const sftp = await getSessionSftp(sshConn); + await listTrash(sftp, getTrashRetentionDays()); + const item = await moveToTrash(sftp, itemPath); + fileLogger.success("Item moved to trash", { + operation: "file_trash_success", + sessionId, + userId, + path: itemPath, + trashId: item.id, + }); + return res.json({ + message: "Item moved to trash", + path: itemPath, + trashItem: item, + }); + } catch (error) { + fileLogger.error("Failed to move item to trash", error, { + operation: "file_trash_failed", + sessionId, + userId, + path: itemPath, + }); + return res.status(409).json({ + error: (error as Error).message, + trashUnavailable: true, + }); + } + } + const { command: deleteCommand, commandWithSuccess } = buildDeleteCommand( itemPath, Boolean(isDirectory), diff --git a/src/backend/hosts/file-manager/transfer-engine.ts b/src/backend/hosts/file-manager/transfer-engine.ts index 4a83a28..5323cc6 100644 --- a/src/backend/hosts/file-manager/transfer-engine.ts +++ b/src/backend/hosts/file-manager/transfer-engine.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import { randomUUID } from "crypto"; import { networkInterfaces } from "os"; import { performance } from "node:perf_hooks"; @@ -18,11 +19,30 @@ import { } from "../transfer-paths.js"; import { buildTransferScanSummary, + estimateIncompressibleSample, getArchiveTransferReasonKey, resolveArchiveTransferMethod, type TransferMethodPreference, type TransferScanSummary, } from "./transfer-routing.js"; +import { verifySftpFileIntegrity } from "./transfer-integrity.js"; +import { + buildDirectProbeCommand, + buildDirectRsyncCommand, + quoteShell, + shouldBenchmarkDirectTransfer, + shouldUseDirectTransfer, + type DirectTransferEndpoint, +} from "./direct-transfer-routing.js"; +import { + getRecentDirectRouteDecision, + getTransferProfile, + initializeTransferProfiles, + recordDirectRouteBenchmark, + recordDirectRouteOutcome, + recordTransferProfile, + selectTransferTuning, +} from "./transfer-tuning.js"; export type { TransferMethodPreference, @@ -53,6 +73,7 @@ export interface SSHSessionLike { userId?: string; ip?: string; port?: number; + username?: string; transferPlatform?: TransferPlatform; } @@ -78,20 +99,16 @@ export interface HostTransferDeps { export type TransferPhase = | "compressing" | "transferring" + | "benchmarking" + | "verifying" | "extracting" | "reconnecting"; export type TransferStatus = - | "running" - | "success" - | "partial" - | "error" - | "cancelled"; -export type TransferMethod = "stream" | "tar" | "item_sftp"; + "running" | "success" | "partial" | "error" | "cancelled"; +export type TransferMethod = "stream" | "tar" | "item_sftp" | "direct_rsync"; export type TransferHopId = - | "source_read" - | "dest_sftp_write" - | "dest_local_write"; + "source_read" | "dest_sftp_write" | "dest_local_write"; export interface TransferHopMetrics { id: TransferHopId; @@ -106,6 +123,9 @@ export interface TransferTimings { compressMs?: number; transferMs?: number; extractMs?: number; + verifyMs?: number; + directBenchmarkMs?: number; + relayBenchmarkMs?: number; sourceDeleteMs?: number; totalMs?: number; transferBytes?: number; @@ -145,6 +165,7 @@ export interface TransferProgress { partialDestRemaining?: boolean; cleanupCompleted?: boolean; retryable?: boolean; + integrityVerified?: boolean; requestSnapshot?: { sourceSessionId: string; sourcePaths: string[]; @@ -164,12 +185,16 @@ export interface TransferRequest { move?: boolean; userId: string; methodPreference?: TransferMethodPreference; - /** Parallel 256 MiB segment lanes for single-file SFTP copy (default 2). */ + /** Explicit parallel lane override; omitted values are tuned automatically. */ parallelSegmentCount?: number; } const activeTransfers = new Map(); const cancelRequestedTransfers = new Set(); +const directRouteCache = new Map< + string, + { useDirect: boolean; expiresAt: number; directMs?: number; relayMs?: number } +>(); /** In-flight pipelined SFTP reads; force-closed when the user cancels. */ interface ActiveXferControl { @@ -323,6 +348,7 @@ interface TransferReconnectContext { dedicatedSourceSessionId: string; dedicatedDestSessionId: string; transferId: string; + profileKey?: string; } type TransferReconnectMeta = Omit< @@ -385,7 +411,8 @@ function buildStreamTransferTimings( const wallStart = progress?.startedAt ?? Date.now(); const totalMs = elapsedMs(wallStart); const prepare = prepareDestMs ?? progress?.timings?.prepareDestMs ?? 0; - const dataMs = Math.max(1, totalMs - prepare); + const verify = progress?.timings?.verifyMs ?? 0; + const dataMs = Math.max(1, totalMs - prepare - verify); return { ...progress?.timings, @@ -403,6 +430,7 @@ async function finalizeStreamTransferIfDestAtSize( destPath: string, expectedSize: number, extra: Partial = {}, + verify?: () => Promise, ): Promise { try { const destSize = await probeDestResumeOffset( @@ -414,6 +442,8 @@ async function finalizeStreamTransferIfDestAtSize( return false; } + await verify?.(); + fileLogger.info("Destination file complete โ€” finalizing transfer", { operation: "host_transfer_dest_complete", transferId, @@ -444,6 +474,7 @@ async function tryFinalizeStreamTransferIfDestComplete( destPath: string, expectedSize: number, extra: Partial = {}, + verify?: () => Promise, ): Promise { try { const destSftp = await deps.getSessionSftp(destSession); @@ -453,6 +484,7 @@ async function tryFinalizeStreamTransferIfDestComplete( destPath, expectedSize, extra, + verify, ); } catch { return false; @@ -796,6 +828,11 @@ function execCommand( deps: HostTransferDeps, session: SSHSessionLike, command: string, + options: { + shouldAbort?: () => boolean; + onOutput?: (chunk: Buffer) => void; + timeoutMs?: number; + } = {}, ): Promise<{ code: number; stderr: string }> { return new Promise((resolve, reject) => { deps.execChannel(session, command, (err, stream) => { @@ -803,17 +840,44 @@ function execCommand( reject(err); return; } + let settled = false; + const startedAt = Date.now(); let stderr = ""; - stream.on("data", () => { - /* consume stdout */ + const abortTimer = + options.shouldAbort || options.timeoutMs + ? setInterval(() => { + const timedOut = + options.timeoutMs !== undefined && + Date.now() - startedAt >= options.timeoutMs; + if ((!options.shouldAbort?.() && !timedOut) || settled) return; + settled = true; + clearInterval(abortTimer); + stream.signal("KILL"); + stream.close(); + reject( + timedOut + ? new Error(`Command timed out after ${options.timeoutMs}ms`) + : new TransferCancelledError(), + ); + }, 250) + : undefined; + stream.on("data", (data: Buffer) => { + options.onOutput?.(data); }); stream.stderr.on("data", (data: Buffer) => { stderr += data.toString(); + options.onOutput?.(data); }); stream.on("close", (code: number) => { + if (settled) return; + settled = true; + if (abortTimer) clearInterval(abortTimer); resolve({ code: code ?? 0, stderr }); }); stream.on("error", (streamErr: Error) => { + if (settled) return; + settled = true; + if (abortTimer) clearInterval(abortTimer); reject(streamErr); }); }); @@ -1097,6 +1161,53 @@ function elapsedMs(start: number): number { return Date.now() - start; } +async function verifyTransferredFile( + deps: HostTransferDeps, + transferId: string, + reconnectMeta: TransferReconnectMeta, + sourcePath: string, + destPath: string, +): Promise { + updateTransfer(transferId, { phase: "verifying" }); + const verifyStart = Date.now(); + const [sourceSession, destSession] = await Promise.all([ + deps.openDedicatedTransferSession( + reconnectMeta.browseSourceSessionId, + reconnectMeta.dedicatedSourceSessionId, + reconnectMeta.userId, + transferId, + { allowBrowseDisconnected: true }, + ), + deps.openDedicatedTransferSession( + reconnectMeta.browseDestSessionId, + reconnectMeta.dedicatedDestSessionId, + reconnectMeta.userId, + transferId, + { allowBrowseDisconnected: true }, + ), + ]); + const [sourceSftp, destSftp] = await Promise.all([ + deps.getSessionSftp(sourceSession), + deps.getSessionSftp(destSession), + ]); + await verifySftpFileIntegrity( + sourceSftp, + destSftp, + sourcePath, + destPath, + createTransferShouldAbort(transferId), + () => new TransferCancelledError(), + ); + const current = activeTransfers.get(transferId); + updateTransfer(transferId, { + integrityVerified: true, + timings: { + ...current?.timings, + verifyMs: (current?.timings?.verifyMs ?? 0) + elapsedMs(verifyStart), + }, + }); +} + export function computeTransferMbPerSec( bytes: number, ms: number, @@ -1293,7 +1404,52 @@ async function scanSourcePathsForRouting( ...work.map((w) => ({ sourcePath: w.sourcePath, size: w.size })), ); } - return buildTransferScanSummary(scanItems); + const summary = buildTransferScanSummary(scanItems); + const candidates = [...scanItems] + .filter((item) => item.size > 0) + .sort((a, b) => b.size - a.size) + .slice(0, 3); + let sampledBytes = 0; + let sampledIncompressibleBytes = 0; + for (const item of candidates) { + throwIfCancelled(transferId); + try { + const sample = await readSftpSample(sftp, item.sourcePath, item.size); + sampledBytes += sample.length; + if (estimateIncompressibleSample(sample)) { + sampledIncompressibleBytes += sample.length; + } + } catch { + /* Extension-based routing remains the safe fallback. */ + } + } + if (sampledBytes > 0) { + summary.sampledIncompressibleRatio = + sampledIncompressibleBytes / sampledBytes; + } + return summary; +} + +async function readSftpSample( + sftp: SFTPWrapper, + path: string, + fileSize: number, +): Promise { + const sampleSize = Math.min(64 * 1024, fileSize); + const position = Math.max(0, Math.floor((fileSize - sampleSize) / 2)); + const handle = await promisifySftpOpen(sftp, path, SFTP_OPEN_READ, 0o666); + try { + const buffer = Buffer.alloc(sampleSize); + const bytesRead = await new Promise((resolve, reject) => { + sftp.read(handle, buffer, 0, sampleSize, position, (err, count) => { + if (err) reject(err); + else resolve(count); + }); + }); + return buffer.subarray(0, bytesRead); + } finally { + await promisifySftpClose(sftp, handle).catch(() => {}); + } } function promisifySftpOpen( @@ -1335,6 +1491,7 @@ interface PipelinedXferOptions { fileSize?: number; initialOffset?: number; parallelSegmentCount?: number; + pipelineConcurrency?: number; onProgress?: (bytes: number) => void; shouldAbort?: () => boolean; transferId?: string; @@ -1449,7 +1606,7 @@ async function runFastSftpCopy( sourceReadClock: ReturnType, destWriteClock: ReturnType, ): Promise { - let concurrency = SFTP_XFER_CONCURRENCY; + let concurrency = options.pipelineConcurrency ?? SFTP_XFER_CONCURRENCY; let chunkSize = SFTP_XFER_CHUNK_SIZE; let bufsize = chunkSize * concurrency; while (bufsize > byteLength && concurrency > 1) { @@ -1703,8 +1860,7 @@ async function runFastSftpCopySegmentedSequential( throw err; } - const message = - err instanceof Error ? err.message : "Segment transfer failed"; + const message = getErrorMessage(err, "Segment transfer failed"); const recoverable = options.reconnect && isRecoverableTransferError(err) && @@ -1996,8 +2152,7 @@ async function runFastSftpCopySegmentedParallel( throw err; } - const message = - err instanceof Error ? err.message : "Segment transfer failed"; + const message = getErrorMessage(err, "Segment transfer failed"); const recoverable = isRecoverableTransferError(err) && attempts < SFTP_PARALLEL_SEGMENT_MAX_ATTEMPTS; @@ -2264,7 +2419,7 @@ async function pipelinedSftpFile( const reset = await resetDedicatedTransferSessions( options.reconnect, attempts, - err instanceof Error ? err.message : "copy failed", + getErrorMessage(err, "copy failed"), ); sftpSource = reset.sourceSftp; sftpDest = reset.destSftp; @@ -2339,7 +2494,7 @@ async function pipelinedSftpToLocalFile( const transferStart = Date.now(); await promisifyFastGet(sourceSftp, sourcePath, localPath, { - concurrency: SFTP_XFER_CONCURRENCY, + concurrency: options.pipelineConcurrency ?? SFTP_XFER_CONCURRENCY, chunkSize: SFTP_XFER_CHUNK_SIZE, fileSize, step: (_total, chunk) => { @@ -2370,6 +2525,13 @@ async function transferFileData( onResumeOffset?: (offset: number) => void, parallelSegmentCount?: number, ): Promise { + const tuning = selectTransferTuning( + fileSize, + reconnect?.profileKey + ? getTransferProfile(reconnect.profileKey) + : undefined, + parallelSegmentCount, + ); const pipeOptions: PipelinedXferOptions = { fileSize, onProgress, @@ -2377,25 +2539,392 @@ async function transferFileData( transferId, reconnect, onResumeOffset, - parallelSegmentCount, + parallelSegmentCount: tuning.parallelSegmentCount, + pipelineConcurrency: tuning.pipelineConcurrency, }; - if (isLocalSshEndpoint(destSession.ip)) { - return pipelinedSftpToLocalFile( - sourceSftp, - sourcePath, - destPath, - pipeOptions, - ); + if (transferId) { + updateTransfer(transferId, { + parallelSegmentCount: tuning.parallelSegmentCount, + }); } - return pipelinedSftpFile( + const startedAt = Date.now(); + const shouldProfile = fileSize >= SFTP_XFER_SEGMENT_THRESHOLD; + fileLogger.info("Selected adaptive transfer tuning", { + operation: "host_transfer_tuning", + transferId, + fileSize, + parallelSegmentCount: tuning.parallelSegmentCount, + pipelineConcurrency: tuning.pipelineConcurrency, + profileSamples: reconnect?.profileKey + ? getTransferProfile(reconnect.profileKey)?.samples + : undefined, + explicitParallelOverride: parallelSegmentCount !== undefined, + }); + try { + const stats = isLocalSshEndpoint(destSession.ip) + ? await pipelinedSftpToLocalFile( + sourceSftp, + sourcePath, + destPath, + pipeOptions, + ) + : await pipelinedSftpFile( + sourceSftp, + destSftp, + sourcePath, + destPath, + pipeOptions, + ); + if (shouldProfile && reconnect?.profileKey) { + recordTransferProfile(reconnect.profileKey, { + bytes: stats.bytes, + durationMs: Math.max(1, Date.now() - startedAt), + lanes: tuning.parallelSegmentCount, + pipelineConcurrency: tuning.pipelineConcurrency, + failed: false, + }); + } + return stats; + } catch (err) { + if (shouldProfile && reconnect?.profileKey) { + recordTransferProfile(reconnect.profileKey, { + bytes: 0, + durationMs: Math.max(1, Date.now() - startedAt), + lanes: tuning.parallelSegmentCount, + pipelineConcurrency: tuning.pipelineConcurrency, + failed: true, + }); + } + throw err; + } +} + +const DIRECT_BENCHMARK_BYTES = 8 * 1024 * 1024; +const DIRECT_ROUTE_CACHE_MS = 10 * 60 * 1000; + +function getDirectEndpoint( + sourceSession: SSHSessionLike, + destSession: SSHSessionLike, +): DirectTransferEndpoint | null { + if ( + !sourceSession.ip || + !destSession.ip || + !destSession.username || + !destSession.port || + sourceSession.transferPlatform !== "unix" || + destSession.transferPlatform !== "unix" + ) { + return null; + } + return { + host: destSession.ip, + port: destSession.port, + username: destSession.username, + }; +} + +function directRouteKey( + sourceSession: SSHSessionLike, + endpoint: DirectTransferEndpoint, +): string { + return `${sourceSession.username ?? ""}@${sourceSession.ip}->${endpoint.username}@${endpoint.host}:${endpoint.port}`; +} + +function transferProfileKey( + sourceSession: SSHSessionLike, + destSession: SSHSessionLike, + userId: string, +): string { + return `${userId}:${sourceSession.username ?? ""}@${sourceSession.ip ?? "local"}:${sourceSession.port ?? 22}->${destSession.username ?? ""}@${destSession.ip ?? "local"}:${destSession.port ?? 22}`; +} + +async function benchmarkTransferRoutes( + deps: HostTransferDeps, + transferId: string, + sourceSession: SSHSessionLike, + destSession: SSHSessionLike, + endpoint: DirectTransferEndpoint, + profileKey?: string, +): Promise<{ useDirect: boolean; directMs?: number; relayMs?: number }> { + const key = directRouteKey(sourceSession, endpoint); + const cached = directRouteCache.get(key); + if (cached && cached.expiresAt > Date.now()) return cached; + const learned = profileKey + ? getRecentDirectRouteDecision(profileKey, DIRECT_ROUTE_CACHE_MS) + : undefined; + if (learned) { + const result = { + ...learned, + expiresAt: Date.now() + DIRECT_ROUTE_CACHE_MS, + }; + directRouteCache.set(key, result); + return result; + } + + updateTransfer(transferId, { phase: "benchmarking" }); + const probe = await execCommand( + deps, + sourceSession, + `command -v rsync >/dev/null && ${buildDirectProbeCommand(endpoint)}`, + { timeoutMs: 8000 }, + ).catch(() => ({ code: 1, stderr: "" })); + if (probe.code !== 0) { + const result = { + useDirect: false, + expiresAt: Date.now() + DIRECT_ROUTE_CACHE_MS, + }; + directRouteCache.set(key, result); + return result; + } + + const probeId = randomUUID(); + const sourceProbe = `/tmp/termix-route-${probeId}.bin`; + const directProbe = `/tmp/termix-route-${probeId}.direct`; + const relayProbe = `/tmp/termix-route-${probeId}.relay`; + const cleanup = async () => { + await Promise.allSettled([ + execCommand(deps, sourceSession, `rm -f ${quoteShell(sourceProbe)}`), + execCommand( + deps, + destSession, + `rm -f ${quoteShell(directProbe)} ${quoteShell(relayProbe)}`, + ), + ]); + }; + + try { + const created = await execCommand( + deps, + sourceSession, + `dd if=/dev/zero of=${quoteShell(sourceProbe)} bs=1048576 count=8 status=none`, + { timeoutMs: 8000 }, + ); + if (created.code !== 0) throw new Error("Failed to create route probe"); + + const directStart = performance.now(); + const direct = await execCommand( + deps, + sourceSession, + buildDirectRsyncCommand(endpoint, [sourceProbe], directProbe, false), + { timeoutMs: 15000 }, + ); + const directMs = performance.now() - directStart; + if (direct.code !== 0) throw new Error("Direct route probe failed"); + + const [sourceSftp, destSftp] = await Promise.all([ + deps.getSessionSftp(sourceSession), + deps.getSessionSftp(destSession), + ]); + const relayStart = performance.now(); + const relayDeadline = Date.now() + 15000; + await transferFileData( + sourceSftp, + destSftp, + destSession, + sourceProbe, + relayProbe, + DIRECT_BENCHMARK_BYTES, + undefined, + () => Date.now() >= relayDeadline, + ); + const relayMs = performance.now() - relayStart; + const profile = profileKey + ? recordDirectRouteBenchmark(profileKey, directMs, relayMs) + : undefined; + const useDirect = + shouldUseDirectTransfer(directMs, relayMs) && + (!profile || profile.outcomeSamples < 2 || profile.failureRate < 0.25); + const result = { + useDirect, + directMs, + relayMs, + expiresAt: Date.now() + DIRECT_ROUTE_CACHE_MS, + }; + directRouteCache.set(key, result); + updateTransfer(transferId, { + timings: { + ...activeTransfers.get(transferId)?.timings, + directBenchmarkMs: directMs, + relayBenchmarkMs: relayMs, + }, + }); + return result; + } catch { + const result = { + useDirect: false, + expiresAt: Date.now() + DIRECT_ROUTE_CACHE_MS, + }; + directRouteCache.set(key, result); + return result; + } finally { + await cleanup(); + } +} + +async function tryAdaptiveDirectTransfer( + deps: HostTransferDeps, + transferId: string, + sourceSession: SSHSessionLike, + destSession: SSHSessionLike, + sourcePaths: string[], + destPath: string, + move: boolean, + reconnectMeta: TransferReconnectMeta, +): Promise { + const endpoint = getDirectEndpoint(sourceSession, destSession); + if (!endpoint) return null; + + const sourceSftp = await deps.getSessionSftp(sourceSession); + const firstStats = await promisifySftpStat(sourceSftp, sourcePaths[0]); + const destIsDirectory = sourcePaths.length > 1 || firstStats.isDirectory(); + const summary = await scanSourcePathsForRouting( sourceSftp, - destSftp, - sourcePath, - destPath, - pipeOptions, + sourcePaths, + transferId, ); + if (!shouldBenchmarkDirectTransfer(summary.totalBytes)) return null; + + const route = await benchmarkTransferRoutes( + deps, + transferId, + sourceSession, + destSession, + endpoint, + reconnectMeta.profileKey, + ); + if (!route.useDirect) return null; + if (destIsDirectory) { + await ensureDestDirectory(deps, destSession, destPath); + } else { + await ensureDestParentForFile(deps, destSession, destPath); + } + + updateTransfer(transferId, { + method: "direct_rsync", + phase: "transferring", + bytesTransferred: 0, + totalBytes: summary.totalBytes, + }); + const transferStart = performance.now(); + let outputBuffer = ""; + + try { + const result = await execCommand( + deps, + sourceSession, + buildDirectRsyncCommand(endpoint, sourcePaths, destPath, destIsDirectory), + { + shouldAbort: createTransferShouldAbort(transferId), + onOutput: (chunk) => { + outputBuffer = `${outputBuffer}${chunk.toString()}`.slice(-2048); + const matches = [...outputBuffer.matchAll(/([\d,]+)\s+(\d+)%/g)]; + const last = matches.at(-1); + if (!last) return; + const bytes = Number(last[1].replace(/,/g, "")); + if (Number.isFinite(bytes)) { + updateTransfer(transferId, { + bytesTransferred: Math.min(bytes, summary.totalBytes), + }); + } + }, + }, + ); + if (result.code !== 0) { + throw new Error(result.stderr || "Direct rsync transfer failed"); + } + + const workItems: FileWorkItem[] = []; + if (destIsDirectory) { + for (const sourcePath of sourcePaths) { + workItems.push( + ...(await collectFileWorkItems(sourceSftp, sourcePath, destPath)), + ); + } + } else { + workItems.push({ + sourcePath: sourcePaths[0], + destPath, + mode: firstStats.mode, + size: firstStats.size, + }); + } + for (const item of workItems) { + await verifyTransferredFile( + deps, + transferId, + reconnectMeta, + item.sourcePath, + item.destPath, + ); + } + if (move) { + await deleteSourcePathsAfterSuccess( + deps, + transferId, + sourceSession, + sourcePaths, + ); + } + + const transferMs = performance.now() - transferStart; + if (reconnectMeta.profileKey) { + recordDirectRouteOutcome( + reconnectMeta.profileKey, + false, + Date.now(), + DIRECT_ROUTE_CACHE_MS, + ); + } + return finalizeTransfer(transferId, { + status: "success", + phase: "verifying", + method: "direct_rsync", + bytesTransferred: summary.totalBytes, + totalBytes: summary.totalBytes, + sourcePaths, + destPath, + sourceDeleted: move, + moveRequested: move, + integrityVerified: true, + timings: { + ...activeTransfers.get(transferId)?.timings, + transferMs, + transferBytes: summary.totalBytes, + endToEndMbPerSec: computeTransferMbPerSec( + summary.totalBytes, + transferMs, + ), + }, + }); + } catch (error) { + if (error instanceof TransferCancelledError) throw error; + fileLogger.warn("Direct transfer failed; falling back to relay", { + operation: "host_transfer_direct_fallback", + transferId, + error: getErrorMessage(error, "Direct transfer failed"), + }); + if (reconnectMeta.profileKey) { + recordDirectRouteOutcome( + reconnectMeta.profileKey, + true, + Date.now(), + DIRECT_ROUTE_CACHE_MS, + ); + } + directRouteCache.set(directRouteKey(sourceSession, endpoint), { + useDirect: false, + expiresAt: Date.now() + DIRECT_ROUTE_CACHE_MS, + }); + updateTransfer(transferId, { + method: undefined, + phase: "transferring", + bytesTransferred: 0, + integrityVerified: false, + }); + return null; + } } async function transferSingleFile( @@ -2407,6 +2936,7 @@ async function transferSingleFile( destPath: string, move: boolean, reconnectMeta: TransferReconnectMeta, + requestedParallelSegments?: number, ): Promise { const sourceSftp = await deps.getSessionSftp(sourceSession); const destSftp = await deps.getSessionSftp(destSession); @@ -2448,9 +2978,12 @@ async function transferSingleFile( updateTransfer(transferId, { bytesTransferred: absoluteOffset }); }; - const parallelLanes = clampParallelSegmentCount( - activeTransfers.get(transferId)?.parallelSegmentCount, + const tuning = selectTransferTuning( + stats.size, + reconnect.profileKey ? getTransferProfile(reconnect.profileKey) : undefined, + requestedParallelSegments, ); + const parallelLanes = tuning.parallelSegmentCount; const useAbsoluteProgressOnly = parallelLanes > 1; throwIfCancelled(transferId); @@ -2472,7 +3005,15 @@ async function transferSingleFile( transferId, reconnect, syncProgress, - parallelLanes, + requestedParallelSegments, + ); + + await verifyTransferredFile( + deps, + transferId, + reconnectMeta, + sourcePath, + destPath, ); if (move) { @@ -2614,8 +3155,16 @@ async function transferViaTar( syncProgress, ); mergeXferStats(xferStats, fileStats); + await verifyTransferredFile( + deps, + transferId, + reconnectMeta, + tempArchive, + tempArchive, + ); } catch (err) { if (!(err instanceof TransferCancelledError)) { + await promisifySftpUnlink(destSftp, tempArchive).catch(() => {}); await cleanupDestItems(deps, destSession, destPath, basenames); } throw err; @@ -2755,6 +3304,14 @@ async function transferViaItemSftp( }, ); mergeXferStats(xferStats, itemXferStats); + await verifyTransferredFile( + deps, + transferId, + reconnectMeta, + item.sourcePath, + item.destPath, + ); + updateTransfer(transferId, { phase: "transferring" }); if (destPlatform !== "windows") { await promisifySftpChmod(destSftp, item.destPath, item.mode); } @@ -2769,6 +3326,7 @@ async function transferViaItemSftp( continue; } if (!(err instanceof TransferCancelledError)) { + await deletePathSftp(destSftp, item.destPath).catch(() => {}); for (const created of [...createdFiles].reverse()) { await deletePathSftp(destSftp, created).catch(() => {}); } @@ -2803,6 +3361,7 @@ async function transferViaItemSftp( destPath, sourceDeleted: status === "success" && move, moveRequested: move, + integrityVerified: status === "success", timings: { ...activeTransfers.get(transferId)?.timings, transferMs, @@ -2816,6 +3375,7 @@ async function runTransfer( transferId: string, request: TransferRequest, ): Promise { + await initializeTransferProfiles(); const { sourceSessionId: browseSourceSessionId, sourcePaths, @@ -2824,13 +3384,10 @@ async function runTransfer( move = false, userId, methodPreference = "auto", - parallelSegmentCount: - requestParallelSegments = DEFAULT_PARALLEL_SEGMENT_COUNT, + parallelSegmentCount: requestParallelSegments, } = request; - const parallelSegmentCount = clampParallelSegmentCount( - requestParallelSegments, - ); + const parallelSegmentCount = requestParallelSegments; const dedicatedSourceSessionId = `xfer:${transferId}:src`; const dedicatedDestSessionId = `xfer:${transferId}:dst`; @@ -2857,11 +3414,18 @@ async function runTransfer( transferId, ); + reconnectMeta.profileKey = transferProfileKey( + sourceSession, + destSession, + userId, + ); + sourceSession.lastActive = Date.now(); destSession.lastActive = Date.now(); updateTransfer(transferId, { - parallelSegmentCount, + parallelSegmentCount: + parallelSegmentCount ?? DEFAULT_PARALLEL_SEGMENT_COUNT, dedicatedSourceSessionId, dedicatedDestSessionId, }); @@ -2896,6 +3460,29 @@ async function runTransfer( } } + const directResult = + methodPreference === "auto" + ? await tryAdaptiveDirectTransfer( + deps, + transferId, + sourceSession, + destSession, + sourcePaths, + destPath, + move, + reconnectMeta, + ) + : null; + if (directResult) { + updateTransfer(transferId, { + timings: { + ...directResult.timings, + totalMs: elapsedMs(runStart), + }, + }); + return; + } + let useArchive = sourcePaths.length > 1; if (sourcePaths.length === 1) { @@ -2913,6 +3500,7 @@ async function runTransfer( destPath, move, reconnectMeta, + parallelSegmentCount, ); } else { const prepareStart = Date.now(); @@ -3016,6 +3604,7 @@ async function runTransfer( totalItems: current?.totalItems, moveRequested: move, sourceDeleted: false, + integrityVerified: false, timings: { ...current?.timings, totalMs: elapsedMs(runStart), @@ -3032,7 +3621,7 @@ async function runTransfer( } const current = activeTransfers.get(transferId); - const message = err instanceof Error ? err.message : "Transfer failed"; + const message = getErrorMessage(err, "Transfer failed"); if ( sourcePaths.length === 1 && @@ -3059,6 +3648,14 @@ async function runTransfer( moveRequested: move, sourceDeleted: current.sourceDeleted, }, + () => + verifyTransferredFile( + deps, + transferId, + reconnectMeta, + sourcePaths[0], + current.destPath!, + ), ); if (finalized) { fileLogger.info("Host transfer succeeded after destination verify", { @@ -3088,6 +3685,7 @@ async function runTransfer( itemsCompleted: current?.itemsCompleted, totalItems: current?.totalItems, moveRequested: move, + integrityVerified: false, timings: { ...current?.timings, totalMs: elapsedMs(runStart), @@ -3253,10 +3851,7 @@ export function retryHostTransfer( await runTransfer(deps, transferId, { ...latest.requestSnapshot, userId, - parallelSegmentCount: - latest.requestSnapshot?.parallelSegmentCount ?? - latest.parallelSegmentCount ?? - DEFAULT_PARALLEL_SEGMENT_COUNT, + parallelSegmentCount: latest.requestSnapshot.parallelSegmentCount, }); })(); @@ -3310,14 +3905,11 @@ export async function previewArchiveTransferMethod( ); const sourceSftp = await deps.getSessionSftp(sourceSession); - const scanItems: Array<{ sourcePath: string; size: number }> = []; - for (const sourcePath of sourcePaths) { - const work = await collectFileWorkItems(sourceSftp, sourcePath, "/"); - scanItems.push( - ...work.map((w) => ({ sourcePath: w.sourcePath, size: w.size })), - ); - } - const scanSummary = buildTransferScanSummary(scanItems); + const scanSummary = await scanSourcePathsForRouting( + sourceSftp, + sourcePaths, + "preview", + ); const sourceHasTar = sourcePlatform === "unix" && (await checkTarAvailable(deps, sourceSession)); @@ -3383,9 +3975,7 @@ export function startHostTransfer( destPath: request.destPath, move: request.move, methodPreference: request.methodPreference, - parallelSegmentCount: clampParallelSegmentCount( - request.parallelSegmentCount, - ), + parallelSegmentCount: request.parallelSegmentCount, }, }); diff --git a/src/backend/hosts/file-manager/transfer-integrity.ts b/src/backend/hosts/file-manager/transfer-integrity.ts new file mode 100644 index 0000000..0dc7052 --- /dev/null +++ b/src/backend/hosts/file-manager/transfer-integrity.ts @@ -0,0 +1,54 @@ +import { createHash } from "node:crypto"; + +type SFTPWrapper = import("ssh2").SFTPWrapper; + +export interface TransferIntegrityResult { + algorithm: "sha256"; + digest: string; +} + +export async function hashSftpFile( + sftp: SFTPWrapper, + path: string, + shouldAbort: () => boolean = () => false, + createAbortError: () => Error = () => new Error("Transfer cancelled"), +): Promise { + const stream = sftp.createReadStream(path); + const hash = createHash("sha256"); + + try { + for await (const chunk of stream) { + if (shouldAbort()) { + const error = createAbortError(); + stream.destroy(error); + throw error; + } + hash.update(chunk); + } + } finally { + stream.destroy(); + } + + if (shouldAbort()) throw createAbortError(); + return hash.digest("hex"); +} + +export async function verifySftpFileIntegrity( + sourceSftp: SFTPWrapper, + destSftp: SFTPWrapper, + sourcePath: string, + destPath: string, + shouldAbort: () => boolean = () => false, + createAbortError: () => Error = () => new Error("Transfer cancelled"), +): Promise { + const [sourceDigest, destDigest] = await Promise.all([ + hashSftpFile(sourceSftp, sourcePath, shouldAbort, createAbortError), + hashSftpFile(destSftp, destPath, shouldAbort, createAbortError), + ]); + + if (sourceDigest !== destDigest) { + throw new Error(`SHA-256 verification failed for ${sourcePath}`); + } + + return { algorithm: "sha256", digest: sourceDigest }; +} diff --git a/src/backend/hosts/file-manager/transfer-routing.ts b/src/backend/hosts/file-manager/transfer-routing.ts index c351742..a4bceb8 100644 --- a/src/backend/hosts/file-manager/transfer-routing.ts +++ b/src/backend/hosts/file-manager/transfer-routing.ts @@ -1,4 +1,5 @@ import type { TransferPlatform } from "../transfer-paths.js"; +import { deflateRawSync } from "node:zlib"; export type TransferMethodPreference = "auto" | "tar" | "item_sftp"; @@ -8,6 +9,8 @@ export interface TransferScanSummary { largestFileBytes: number; /** Share of total bytes in likely incompressible file types (0โ€“1). */ incompressibleRatio: number; + /** Share inferred from bounded content samples, when available. */ + sampledIncompressibleRatio?: number; } const INCOMPRESSIBLE_EXT = @@ -43,6 +46,15 @@ export function buildTransferScanSummary( }; } +export function estimateIncompressibleSample(data: Buffer): boolean { + if (data.length === 0) return false; + return deflateRawSync(data, { level: 1 }).length / data.length >= 0.9; +} + +function effectiveIncompressibleRatio(summary: TransferScanSummary): number { + return summary.sampledIncompressibleRatio ?? summary.incompressibleRatio; +} + /** * Choose tar vs per-item SFTP for directory / multi-file transfers. * Single-file stream transfers bypass this entirely. @@ -72,8 +84,8 @@ export function resolveArchiveTransferMethod( return "item_sftp"; } - const { fileCount, totalBytes, largestFileBytes, incompressibleRatio } = - summary; + const { fileCount, totalBytes, largestFileBytes } = summary; + const incompressibleRatio = effectiveIncompressibleRatio(summary); if (fileCount === 0) { return "item_sftp"; @@ -154,8 +166,8 @@ export function getArchiveTransferReasonKey( return "tar_unavailable"; } - const { fileCount, totalBytes, largestFileBytes, incompressibleRatio } = - summary; + const { fileCount, totalBytes, largestFileBytes } = summary; + const incompressibleRatio = effectiveIncompressibleRatio(summary); if ( fileCount > 1 && diff --git a/src/backend/hosts/file-manager/transfer-tuning.ts b/src/backend/hosts/file-manager/transfer-tuning.ts new file mode 100644 index 0000000..1b2148d --- /dev/null +++ b/src/backend/hosts/file-manager/transfer-tuning.ts @@ -0,0 +1,387 @@ +import { createHash } from "node:crypto"; +import { promises as fs } from "node:fs"; +import path from "node:path"; + +export interface TransferPerformanceProfile { + throughputBps: number; + failureRate: number; + samples: number; + preferredLanes: number; + pipelineConcurrency: number; + updatedAt: number; +} + +export interface TransferTuning { + parallelSegmentCount: number; + pipelineConcurrency: number; +} + +export interface DirectRouteProfile { + directMs: number; + relayMs: number; + failureRate: number; + benchmarkSamples: number; + outcomeSamples: number; + benchmarkedAt: number; + cooldownUntil?: number; + updatedAt: number; +} + +const MB = 1024 * 1024; +const PROFILE_TTL_MS = 7 * 24 * 60 * 60 * 1000; +const MAX_PROFILES = 128; +const STORE_VERSION = 1; +const STORE_FILENAME = "adaptive-transfer-profiles.json"; +const profiles = new Map(); +const directRoutes = new Map(); +let loadedPath: string | undefined; +let loadPromise: Promise | undefined; +let persistTimer: ReturnType | undefined; +let persistPromise = Promise.resolve(); + +interface PersistedProfiles { + version: typeof STORE_VERSION; + profiles: Record; + directRoutes: Record; +} + +function validDirectRoute(value: unknown): value is DirectRouteProfile { + if (!value || typeof value !== "object") return false; + const profile = value as Partial; + return ( + Number.isFinite(profile.directMs) && + Number(profile.directMs) > 0 && + Number.isFinite(profile.relayMs) && + Number(profile.relayMs) > 0 && + Number.isFinite(profile.failureRate) && + Number(profile.failureRate) >= 0 && + Number(profile.failureRate) <= 1 && + Number.isFinite(profile.benchmarkSamples) && + Number(profile.benchmarkSamples) > 0 && + Number.isFinite(profile.outcomeSamples) && + Number(profile.outcomeSamples) >= 0 && + Number.isFinite(profile.benchmarkedAt) && + (profile.cooldownUntil === undefined || + Number.isFinite(profile.cooldownUntil)) && + Number.isFinite(profile.updatedAt) + ); +} + +function storePath(): string { + const dataDir = + process.env.DATA_DIR || path.join(process.cwd(), "db", "data"); + return path.join(dataDir, STORE_FILENAME); +} + +function profileId(key: string): string { + return createHash("sha256").update(key).digest("hex"); +} + +function validProfile(value: unknown): value is TransferPerformanceProfile { + if (!value || typeof value !== "object") return false; + const profile = value as Partial; + return ( + Number.isFinite(profile.throughputBps) && + Number(profile.throughputBps) >= 0 && + Number.isFinite(profile.failureRate) && + Number(profile.failureRate) >= 0 && + Number(profile.failureRate) <= 1 && + Number.isFinite(profile.samples) && + Number(profile.samples) > 0 && + Number.isFinite(profile.preferredLanes) && + Number.isFinite(profile.pipelineConcurrency) && + Number.isFinite(profile.updatedAt) + ); +} + +function trimProfiles(now = Date.now()): void { + for (const [key, profile] of profiles) { + if (now - profile.updatedAt > PROFILE_TTL_MS) profiles.delete(key); + } + const recent = [...profiles.entries()] + .sort(([, a], [, b]) => b.updatedAt - a.updatedAt) + .slice(0, MAX_PROFILES); + profiles.clear(); + for (const [key, profile] of recent) profiles.set(key, profile); + + for (const [key, profile] of directRoutes) { + if (now - profile.updatedAt > PROFILE_TTL_MS) directRoutes.delete(key); + } + const recentRoutes = [...directRoutes.entries()] + .sort(([, a], [, b]) => b.updatedAt - a.updatedAt) + .slice(0, MAX_PROFILES); + directRoutes.clear(); + for (const [key, profile] of recentRoutes) directRoutes.set(key, profile); +} + +async function persistProfiles(): Promise { + trimProfiles(); + const target = storePath(); + const temporary = `${target}.${process.pid}.tmp`; + const payload: PersistedProfiles = { + version: STORE_VERSION, + profiles: Object.fromEntries(profiles), + directRoutes: Object.fromEntries(directRoutes), + }; + try { + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.writeFile(temporary, JSON.stringify(payload), { + encoding: "utf8", + mode: 0o600, + }); + await fs.rename(temporary, target); + } catch { + await fs.rm(temporary, { force: true }).catch(() => {}); + } +} + +function queuePersist(): void { + if (persistTimer) clearTimeout(persistTimer); + persistTimer = setTimeout(() => { + persistTimer = undefined; + persistPromise = persistPromise.then(persistProfiles); + }, 250); + persistTimer.unref?.(); +} + +export async function initializeTransferProfiles(): Promise { + const target = storePath(); + if (loadedPath === target) return loadPromise; + loadedPath = target; + loadPromise = (async () => { + profiles.clear(); + directRoutes.clear(); + try { + const parsed = JSON.parse( + await fs.readFile(target, "utf8"), + ) as Partial; + if (parsed.version !== STORE_VERSION || !parsed.profiles) return; + for (const [key, profile] of Object.entries(parsed.profiles)) { + if (/^[a-f0-9]{64}$/.test(key) && validProfile(profile)) { + profiles.set(key, profile); + } + } + for (const [key, profile] of Object.entries(parsed.directRoutes ?? {})) { + if (/^[a-f0-9]{64}$/.test(key) && validDirectRoute(profile)) { + directRoutes.set(key, profile); + } + } + trimProfiles(); + } catch { + // Missing or malformed local learning data must not affect transfers. + } + })(); + return loadPromise; +} + +export async function flushTransferProfiles(): Promise { + if (persistTimer) { + clearTimeout(persistTimer); + persistTimer = undefined; + } + persistPromise = persistPromise.then(persistProfiles); + await persistPromise; +} + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, Math.floor(value))); +} + +export function selectTransferTuning( + fileSize: number, + profile?: TransferPerformanceProfile, + requestedLanes?: number, +): TransferTuning { + if (fileSize < 32 * MB) { + return { parallelSegmentCount: 1, pipelineConcurrency: 8 }; + } + + let lanes = fileSize >= 1024 * MB ? 4 : 2; + let pipelineConcurrency = fileSize >= 256 * MB ? 32 : 16; + + if (profile) { + lanes = profile.preferredLanes; + pipelineConcurrency = profile.pipelineConcurrency; + if (profile.failureRate >= 0.2) { + lanes = Math.min(lanes, 2); + pipelineConcurrency = Math.min(pipelineConcurrency, 16); + } + } + + if (requestedLanes !== undefined) lanes = requestedLanes; + + const segmentCapacity = Math.max(1, Math.ceil(fileSize / (256 * MB))); + return { + parallelSegmentCount: clamp(lanes, 1, Math.min(8, segmentCapacity)), + pipelineConcurrency: clamp(pipelineConcurrency, 8, 64), + }; +} + +export function updateTransferProfile( + previous: TransferPerformanceProfile | undefined, + observation: { + bytes: number; + durationMs: number; + lanes: number; + pipelineConcurrency: number; + failed: boolean; + now?: number; + }, +): TransferPerformanceProfile { + const now = observation.now ?? Date.now(); + const throughputBps = + observation.durationMs > 0 + ? (observation.bytes * 1000) / observation.durationMs + : 0; + const weight = previous ? 0.25 : 1; + const smoothedThroughput = previous + ? previous.throughputBps * (1 - weight) + throughputBps * weight + : throughputBps; + const failure = observation.failed ? 1 : 0; + const failureRate = previous + ? previous.failureRate * (1 - weight) + failure * weight + : failure; + + let preferredLanes = observation.lanes; + let pipelineConcurrency = observation.pipelineConcurrency; + if (failureRate >= 0.2) { + preferredLanes = Math.max(1, Math.floor(preferredLanes / 2)); + pipelineConcurrency = Math.max(8, Math.floor(pipelineConcurrency / 2)); + } else if ( + !observation.failed && + previous && + previous.samples >= 2 && + throughputBps > previous.throughputBps * 1.25 + ) { + preferredLanes = Math.min(8, preferredLanes + 1); + pipelineConcurrency = Math.min(64, pipelineConcurrency + 8); + } + + return { + throughputBps: smoothedThroughput, + failureRate, + samples: (previous?.samples ?? 0) + 1, + preferredLanes, + pipelineConcurrency, + updatedAt: now, + }; +} + +export function getTransferProfile( + key: string, + now = Date.now(), +): TransferPerformanceProfile | undefined { + const id = profileId(key); + const profile = profiles.get(id); + if (!profile) return undefined; + if (now - profile.updatedAt <= PROFILE_TTL_MS) return profile; + profiles.delete(id); + queuePersist(); + return undefined; +} + +export function recordTransferProfile( + key: string, + observation: Parameters[1], +): TransferPerformanceProfile { + const profile = updateTransferProfile(getTransferProfile(key), observation); + profiles.set(profileId(key), profile); + trimProfiles(observation.now); + queuePersist(); + return profile; +} + +export function getDirectRouteProfile( + key: string, + now = Date.now(), +): DirectRouteProfile | undefined { + const id = profileId(key); + const profile = directRoutes.get(id); + if (!profile) return undefined; + if (now - profile.updatedAt <= PROFILE_TTL_MS) return profile; + directRoutes.delete(id); + queuePersist(); + return undefined; +} + +export function recordDirectRouteBenchmark( + key: string, + directMs: number, + relayMs: number, + now = Date.now(), +): DirectRouteProfile { + const previous = getDirectRouteProfile(key, now); + const weight = previous ? 0.25 : 1; + const profile: DirectRouteProfile = { + directMs: previous + ? previous.directMs * (1 - weight) + directMs * weight + : directMs, + relayMs: previous + ? previous.relayMs * (1 - weight) + relayMs * weight + : relayMs, + failureRate: previous?.failureRate ?? 0, + benchmarkSamples: (previous?.benchmarkSamples ?? 0) + 1, + outcomeSamples: previous?.outcomeSamples ?? 0, + benchmarkedAt: now, + cooldownUntil: previous?.cooldownUntil, + updatedAt: now, + }; + directRoutes.set(profileId(key), profile); + trimProfiles(now); + queuePersist(); + return profile; +} + +export function recordDirectRouteOutcome( + key: string, + failed: boolean, + now = Date.now(), + cooldownMs = 10 * 60 * 1000, +): DirectRouteProfile | undefined { + const previous = getDirectRouteProfile(key, now); + if (!previous) return undefined; + const failure = failed ? 1 : 0; + const weight = previous.outcomeSamples > 0 ? 0.25 : 1; + const profile = { + ...previous, + failureRate: previous.failureRate * (1 - weight) + failure * weight, + outcomeSamples: previous.outcomeSamples + 1, + cooldownUntil: failed ? now + cooldownMs : undefined, + updatedAt: now, + }; + directRoutes.set(profileId(key), profile); + queuePersist(); + return profile; +} + +export function getRecentDirectRouteDecision( + key: string, + maxAgeMs: number, + now = Date.now(), +): { useDirect: boolean; directMs: number; relayMs: number } | undefined { + const profile = getDirectRouteProfile(key, now); + if (!profile) return undefined; + if ((profile.cooldownUntil ?? 0) > now) { + return { + useDirect: false, + directMs: profile.directMs, + relayMs: profile.relayMs, + }; + } + if (now - profile.benchmarkedAt > maxAgeMs) return undefined; + return { + useDirect: + profile.failureRate < 0.2 && profile.directMs <= profile.relayMs * 0.8, + directMs: profile.directMs, + relayMs: profile.relayMs, + }; +} + +export function clearTransferProfiles(): void { + if (persistTimer) clearTimeout(persistTimer); + persistTimer = undefined; + loadedPath = undefined; + loadPromise = undefined; + profiles.clear(); + directRoutes.clear(); +} diff --git a/src/backend/hosts/file-manager/trash-service.ts b/src/backend/hosts/file-manager/trash-service.ts new file mode 100644 index 0000000..6b0705d --- /dev/null +++ b/src/backend/hosts/file-manager/trash-service.ts @@ -0,0 +1,234 @@ +import crypto from "node:crypto"; +import path from "node:path"; +import type { SFTPWrapper, Stats } from "ssh2"; + +export interface TrashItem { + id: string; + name: string; + originalPath: string; + isDirectory: boolean; + deletedAt: string; + size: number; +} + +type StoredTrashItem = TrashItem & { trashPath: string }; + +const TRASH_DIR = ".termix-trash"; +const ID_PATTERN = /^[0-9a-f-]{36}$/i; + +function call( + run: (done: (error: Error | undefined, value: T) => void) => void, +) { + return new Promise((resolve, reject) => { + run((error, value) => (error ? reject(error) : resolve(value))); + }); +} + +function stat(sftp: SFTPWrapper, target: string) { + return call((done) => sftp.stat(target, done)); +} + +function lstat(sftp: SFTPWrapper, target: string) { + return call((done) => sftp.lstat(target, done)); +} + +function readdir(sftp: SFTPWrapper, target: string) { + return call((done) => sftp.readdir(target, done)); +} + +function readFile(sftp: SFTPWrapper, target: string) { + return call((done) => sftp.readFile(target, done)); +} + +function writeFile(sftp: SFTPWrapper, target: string, data: string) { + return call((done) => sftp.writeFile(target, data, done)); +} + +function rename(sftp: SFTPWrapper, from: string, to: string) { + return call((done) => sftp.rename(from, to, done)); +} + +function unlink(sftp: SFTPWrapper, target: string) { + return call((done) => sftp.unlink(target, done)); +} + +function mkdir(sftp: SFTPWrapper, target: string) { + return call((done) => sftp.mkdir(target, done)); +} + +function rmdir(sftp: SFTPWrapper, target: string) { + return call((done) => sftp.rmdir(target, done)); +} + +async function exists(sftp: SFTPWrapper, target: string) { + try { + await stat(sftp, target); + return true; + } catch { + return false; + } +} + +async function ensureDirectory(sftp: SFTPWrapper, target: string) { + const normalized = target.replace(/\\/g, "/"); + const root = normalized.startsWith("/") ? "/" : ""; + const parts = normalized.split("/").filter(Boolean); + let current = root; + for (const part of parts) { + current = + current === "/" ? `/${part}` : current ? `${current}/${part}` : part; + if (await exists(sftp, current)) continue; + await mkdir(sftp, current); + } +} + +async function removeTree(sftp: SFTPWrapper, target: string): Promise { + const targetStat = await lstat(sftp, target); + if (!targetStat.isDirectory()) { + await unlink(sftp, target); + return; + } + for (const entry of await readdir(sftp, target)) { + if (entry.filename === "." || entry.filename === "..") continue; + await removeTree(sftp, path.posix.join(target, entry.filename)); + } + await rmdir(sftp, target); +} + +async function trashPaths(sftp: SFTPWrapper) { + const home = await call((done) => sftp.realpath(".", done)); + const root = path.posix.join(home.replace(/\\/g, "/"), TRASH_DIR); + const files = path.posix.join(root, "files"); + const info = path.posix.join(root, "info"); + await ensureDirectory(sftp, files); + await ensureDirectory(sftp, info); + return { root, files, info }; +} + +export function isSafeTrashSource(itemPath: string, trashRoot: string) { + const normalized = path.posix.normalize(itemPath.replace(/\\/g, "/")); + const root = path.posix.normalize(trashRoot); + return ( + normalized !== "." && + normalized !== "/" && + !/^[A-Za-z]:\/?$/.test(normalized) && + normalized !== root && + !normalized.startsWith(`${root}/`) + ); +} + +function publicItem(item: StoredTrashItem): TrashItem { + const { trashPath: _trashPath, ...result } = item; + return result; +} + +async function readStoredItem( + sftp: SFTPWrapper, + dirs: { root: string; files: string; info: string }, + id: string, +): Promise { + if (!ID_PATTERN.test(id)) throw new Error("Invalid trash item id"); + const parsed = JSON.parse( + (await readFile(sftp, path.posix.join(dirs.info, `${id}.json`))).toString( + "utf8", + ), + ) as StoredTrashItem; + if ( + parsed.id !== id || + parsed.trashPath !== path.posix.join(dirs.files, id) || + !isSafeTrashSource(parsed.originalPath, dirs.root) + ) { + throw new Error("Invalid trash metadata"); + } + return parsed; +} + +export async function moveToTrash( + sftp: SFTPWrapper, + itemPath: string, +): Promise { + const dirs = await trashPaths(sftp); + if (!isSafeTrashSource(itemPath, dirs.root)) { + throw new Error("This path cannot be moved to trash"); + } + const itemStat = await lstat(sftp, itemPath); + const id = crypto.randomUUID(); + const trashPath = path.posix.join(dirs.files, id); + const item: StoredTrashItem = { + id, + name: path.posix.basename(itemPath.replace(/\\/g, "/")), + originalPath: itemPath, + trashPath, + isDirectory: itemStat.isDirectory(), + deletedAt: new Date().toISOString(), + size: itemStat.size, + }; + + await rename(sftp, itemPath, trashPath); + try { + await writeFile( + sftp, + path.posix.join(dirs.info, `${id}.json`), + JSON.stringify(item), + ); + } catch (error) { + await rename(sftp, trashPath, itemPath).catch(() => {}); + throw error; + } + return publicItem(item); +} + +export async function listTrash( + sftp: SFTPWrapper, + retentionDays: number, +): Promise { + const dirs = await trashPaths(sftp); + const cutoff = Date.now() - retentionDays * 86_400_000; + const items: TrashItem[] = []; + for (const entry of await readdir(sftp, dirs.info)) { + if (!entry.filename.endsWith(".json")) continue; + const id = entry.filename.slice(0, -5); + try { + const item = await readStoredItem(sftp, dirs, id); + if (new Date(item.deletedAt).getTime() < cutoff) { + if (await exists(sftp, item.trashPath)) + await removeTree(sftp, item.trashPath); + await unlink(sftp, path.posix.join(dirs.info, entry.filename)); + continue; + } + if (await exists(sftp, item.trashPath)) items.push(publicItem(item)); + else await unlink(sftp, path.posix.join(dirs.info, entry.filename)); + } catch { + // Ignore corrupt metadata without exposing arbitrary paths to deletion. + } + } + return items.sort((a, b) => b.deletedAt.localeCompare(a.deletedAt)); +} + +export async function restoreTrashItem(sftp: SFTPWrapper, id: string) { + const dirs = await trashPaths(sftp); + const item = await readStoredItem(sftp, dirs, id); + if (await exists(sftp, item.originalPath)) { + throw new Error("A file already exists at the original path"); + } + await rename(sftp, item.trashPath, item.originalPath); + await unlink(sftp, path.posix.join(dirs.info, `${id}.json`)); + return publicItem(item); +} + +export async function permanentlyDeleteTrashItem( + sftp: SFTPWrapper, + id: string, +) { + const dirs = await trashPaths(sftp); + const item = await readStoredItem(sftp, dirs, id); + if (await exists(sftp, item.trashPath)) + await removeTree(sftp, item.trashPath); + await unlink(sftp, path.posix.join(dirs.info, `${id}.json`)); +} + +export async function emptyTrash(sftp: SFTPWrapper) { + const items = await listTrash(sftp, 365_000); + for (const item of items) await permanentlyDeleteTrashItem(sftp, item.id); + return items.length; +} diff --git a/src/backend/hosts/guacamole/guacamole-server.ts b/src/backend/hosts/guacamole/guacamole-server.ts index 1d142e2..25e3ad3 100644 --- a/src/backend/hosts/guacamole/guacamole-server.ts +++ b/src/backend/hosts/guacamole/guacamole-server.ts @@ -1,12 +1,16 @@ import GuacamoleLite from "guacamole-lite"; import { guacLogger } from "../../utils/logger.js"; -import { GuacamoleTokenService } from "./token-service.js"; -import { getCurrentSettingValue } from "../../database/repositories/factory.js"; +import { + GuacamoleTokenService, + type GuacamoleRecordingMetadata, +} from "./token-service.js"; +import { + createCurrentSessionRecordingRepository, + getCurrentSettingValue, +} from "../../database/repositories/factory.js"; import { resolveGuacdOptions } from "../../utils/guacd-config.js"; import fs from "fs"; import path from "path"; -import { createCurrentSessionRecordingRepository } from "../../database/repositories/factory.js"; -import type { GuacamoleRecordingMetadata } from "./token-service.js"; const tokenService = GuacamoleTokenService.getInstance(); @@ -27,12 +31,64 @@ const GUACAMOLE_RECORDINGS_DIR = path.join(DATA_DIR, "session_recordings", "guacamole"); type GuacamoleClientConnection = { + guacamoleConnectionId?: string; connectionSettings?: { - connection?: { type?: string }; + connection?: { type?: string; join?: string; readOnly?: boolean }; recording?: GuacamoleRecordingMetadata; + termixMeta?: { + termixConnectId: string; + hostId: number; + ownerUserId: string; + protocol: string; + }; }; }; +export interface GuacSessionInfo { + guacamoleConnectionId: string; + hostId: number; + ownerUserId: string; + protocol: string; + openedAt: number; +} + +// Keyed by termixConnectId (routes.ts's correlation id), populated once the +// primary connection's guacd handshake completes. +const guacSessionByConnectId = new Map(); +// Keyed by guacd's own guacamoleConnectionId, for join-time lookups. +const guacSessionByGuacamoleId = new Map(); +const pendingConnectResolvers = new Map< + string, + (info: GuacSessionInfo | null) => void +>(); + +export function waitForGuacdOpen( + termixConnectId: string, + timeoutMs = 10000, +): Promise { + const existing = guacSessionByConnectId.get(termixConnectId); + if (existing) return Promise.resolve(existing); + + return new Promise((resolve) => { + let settled = false; + const finish = (info: GuacSessionInfo | null) => { + if (settled) return; + settled = true; + pendingConnectResolvers.delete(termixConnectId); + resolve(info); + }; + + pendingConnectResolvers.set(termixConnectId, finish); + setTimeout(() => finish(null), timeoutMs); + }); +} + +export function getGuacSessionInfo( + guacamoleConnectionId: string, +): GuacSessionInfo | null { + return guacSessionByGuacamoleId.get(guacamoleConnectionId) ?? null; +} + async function persistGuacamoleRecording( clientConnection: GuacamoleClientConnection, ): Promise { @@ -52,10 +108,16 @@ async function persistGuacamoleRecording( await new Promise((resolve) => setTimeout(resolve, 100)); } if (!fs.existsSync(resolvedPath)) { + const guacdPath = recording.guacdPath ?? GUACAMOLE_RECORDINGS_DIR; guacLogger.warn("Guacamole recording file was not found", { operation: "guac_recording_missing", hostId: recording.hostId, path: resolvedPath, + guacdPath, + hint: + "guacd writes the recording to guacdPath, the backend reads it from path. " + + "When guacd runs in its own container these must be the same volume โ€” set " + + "GUACD_RECORDING_PATH to guacd's mount point and GUACD_RECORDING_BACKEND_PATH to this one.", }); return; } @@ -118,9 +180,13 @@ const clientOptions = { vnc: { "swap-red-blue": false, cursor: "remote", - security: "any", width: 1280, height: 720, + // macOS Screen Sharing negotiates its VNC security type over several + // round trips (RFB type 30 -> 33/36/2/35) and can fail the first + // attempt; retrying lets guacd's VNC client re-establish instead of + // guacd giving up immediately (Support#1063). + autoretry: 2, }, telnet: { "terminal-type": "xterm-256color", @@ -149,6 +215,25 @@ function createGuacServer(): GuacamoleLite { operation: "guac_connection_open", type: clientConnection.connectionSettings?.connection?.type, }); + + const termixMeta = clientConnection.connectionSettings?.termixMeta; + const guacamoleConnectionId = clientConnection.guacamoleConnectionId; + const isJoin = !!clientConnection.connectionSettings?.connection?.join; + + if (!isJoin && termixMeta && guacamoleConnectionId) { + const info: GuacSessionInfo = { + guacamoleConnectionId, + hostId: termixMeta.hostId, + ownerUserId: termixMeta.ownerUserId, + protocol: termixMeta.protocol, + openedAt: Date.now(), + }; + guacSessionByConnectId.set(termixMeta.termixConnectId, info); + guacSessionByGuacamoleId.set(guacamoleConnectionId, info); + + const resolver = pendingConnectResolvers.get(termixMeta.termixConnectId); + if (resolver) resolver(info); + } }); server.on("close", (clientConnection: GuacamoleClientConnection) => { @@ -156,6 +241,15 @@ function createGuacServer(): GuacamoleLite { operation: "guac_connection_close", type: clientConnection.connectionSettings?.connection?.type, }); + + const isJoin = !!clientConnection.connectionSettings?.connection?.join; + const termixMeta = clientConnection.connectionSettings?.termixMeta; + const guacamoleConnectionId = clientConnection.guacamoleConnectionId; + if (!isJoin && termixMeta && guacamoleConnectionId) { + guacSessionByConnectId.delete(termixMeta.termixConnectId); + guacSessionByGuacamoleId.delete(guacamoleConnectionId); + } + persistGuacamoleRecording(clientConnection).catch((error) => { guacLogger.error("Failed to persist Guacamole recording", error, { operation: "guac_recording_persist_error", diff --git a/src/backend/hosts/guacamole/jump-tunnel-endpoint.ts b/src/backend/hosts/guacamole/jump-tunnel-endpoint.ts new file mode 100644 index 0000000..e05f1e2 --- /dev/null +++ b/src/backend/hosts/guacamole/jump-tunnel-endpoint.ts @@ -0,0 +1,15 @@ +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); + +export function resolveJumpTunnelEndpoint( + guacdHost: string, + tunnelHost = process.env.GUACD_TUNNEL_HOST, +): { bindHost: string; advertisedHost: string } { + if (LOOPBACK_HOSTS.has(guacdHost.toLowerCase())) { + return { bindHost: "127.0.0.1", advertisedHost: "127.0.0.1" }; + } + + return { + bindHost: "0.0.0.0", + advertisedHost: tunnelHost?.trim() || "termix", + }; +} diff --git a/src/backend/hosts/guacamole/rdp-settings.ts b/src/backend/hosts/guacamole/rdp-settings.ts new file mode 100644 index 0000000..63ada8e --- /dev/null +++ b/src/backend/hosts/guacamole/rdp-settings.ts @@ -0,0 +1,36 @@ +interface RdpSettingsInput { + port: number; + domain?: string; + security?: string; + ignoreCert: boolean; + guacConfig: Record; + guacdOverrides: Record; +} + +export function buildRdpSettings({ + port, + domain, + security, + ignoreCert, + guacConfig, + guacdOverrides, +}: RdpSettingsInput): Record { + return { + ...guacConfig, + port, + domain, + ...(security === undefined ? {} : { security }), + "ignore-cert": ignoreCert, + ...guacdOverrides, + }; +} + +export function resolveRdpDomain( + authType: string | null, + promptedDomain: unknown, + storedDomain: string, +): string { + return authType === "none" && typeof promptedDomain === "string" + ? promptedDomain + : storedDomain; +} diff --git a/src/backend/hosts/guacamole/recording-settings.ts b/src/backend/hosts/guacamole/recording-settings.ts new file mode 100644 index 0000000..3236916 --- /dev/null +++ b/src/backend/hosts/guacamole/recording-settings.ts @@ -0,0 +1,22 @@ +/** + * Merges Termix's recording bookkeeping into a host's guacd settings. + * + * Location and filename are not the host's to choose: recordings are indexed by + * them for playback, and the backend refuses to read anything outside its + * recordings directory. What a recording *contains* is a host-level decision, so + * those flags are only defaulted, never overwritten. + */ +export function withRecordingSettings( + guacConfig: Record, + recordingPath: string, + recordingName: string, +): Record { + return { + ...guacConfig, + "recording-path": recordingPath, + "recording-name": recordingName, + "create-recording-path": true, + "recording-exclude-output": guacConfig["recording-exclude-output"] ?? false, + "recording-include-keys": guacConfig["recording-include-keys"] ?? true, + }; +} diff --git a/src/backend/hosts/guacamole/routes.ts b/src/backend/hosts/guacamole/routes.ts index 0e47c2f..628f506 100644 --- a/src/backend/hosts/guacamole/routes.ts +++ b/src/backend/hosts/guacamole/routes.ts @@ -1,9 +1,10 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import express from "express"; import { GuacamoleTokenService } from "./token-service.js"; +import { withRecordingSettings } from "./recording-settings.js"; import { guacLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { PermissionManager } from "../../utils/permission-manager.js"; -import { Client } from "ssh2"; import net from "net"; import crypto from "crypto"; import path from "path"; @@ -13,6 +14,15 @@ import { createCurrentSettingsRepository, } from "../../database/repositories/factory.js"; import { resolveGuacdOptions } from "../../utils/guacd-config.js"; +import { createJumpHostChain } from "../jump-host-chain.js"; +import { waitForGuacdOpen } from "./guacamole-server.js"; +import { + logAudit, + getAuditUsername, + getRequestMeta, +} from "../../utils/audit-logger.js"; +import { resolveJumpTunnelEndpoint } from "./jump-tunnel-endpoint.js"; +import { buildRdpSettings, resolveRdpDomain } from "./rdp-settings.js"; const router = express.Router(); const tokenService = GuacamoleTokenService.getInstance(); @@ -165,6 +175,12 @@ router.post("/token", async (req, res) => { * type: string * enum: [rdp, vnc, telnet] * description: Override the host's default connection type + * promptedUsername: + * type: string + * description: Username for this connection only, used when the host's RDP auth type is "none". Not persisted. + * promptedPassword: + * type: string + * description: Password for this connection only, used when the host's RDP auth type is "none". Not persisted. * responses: * 200: * description: Connection token generated successfully @@ -176,6 +192,10 @@ router.post("/token", async (req, res) => { * token: * type: string * description: Encrypted connection token + * guacamoleConnectionId: + * type: string + * nullable: true + * description: guacd's own connection id for this session, once the handshake completes. Used to mint session-share join tokens. * 400: * description: Invalid request or unsupported connection type * 403: @@ -266,7 +286,7 @@ router.post( guacLogger.warn("Failed to parse guacamole config", { operation: "guac_config_parse_error", hostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -281,8 +301,7 @@ router.post( // Extract per-connection guacd proxy settings before passing the rest as connection settings const perConnectionGuacdHost = guacConfig["guacd-hostname"] as - | string - | undefined; + string | undefined; const perConnectionGuacdPortRaw = guacConfig["guacd-port"]; const perConnectionGuacdPort = perConnectionGuacdPortRaw ? parseInt(String(perConnectionGuacdPortRaw), 10) || undefined @@ -338,7 +357,7 @@ router.post( operation: "guac_shared_secret_resolve", hostId, protocol: connectionType, - error: e instanceof Error ? e.message : "Unknown", + error: getErrorMessage(e, "Unknown"), }); } } else { @@ -368,7 +387,7 @@ router.post( guacLogger.warn("Failed to resolve RDP credential", { operation: "guac_rdp_credential_resolve", hostId, - error: e instanceof Error ? e.message : "Unknown", + error: getErrorMessage(e, "Unknown"), }); } } @@ -387,7 +406,7 @@ router.post( guacLogger.warn("Failed to resolve VNC credential", { operation: "guac_vnc_credential_resolve", hostId, - error: e instanceof Error ? e.message : "Unknown", + error: getErrorMessage(e, "Unknown"), }); } } @@ -409,7 +428,7 @@ router.post( guacLogger.warn("Failed to resolve Telnet credential", { operation: "guac_telnet_credential_resolve", hostId, - error: e instanceof Error ? e.message : "Unknown", + error: getErrorMessage(e, "Unknown"), }); } } @@ -421,12 +440,22 @@ router.post( let username: string; let password: string; + const rdpAuthTypeForConnect = isSharedConnection + ? null + : (host.rdpAuthType as string) || + (host.rdpCredentialId ? "credential" : "direct"); + switch (connectionType) { case "rdp": - username = - (host.rdpUser as string) || (host.username as string) || ""; - password = - (host.rdpPassword as string) || (host.password as string) || ""; + if (rdpAuthTypeForConnect === "none") { + username = String(req.body?.promptedUsername || ""); + password = String(req.body?.promptedPassword || ""); + } else { + username = + (host.rdpUser as string) || (host.username as string) || ""; + password = + (host.rdpPassword as string) || (host.password as string) || ""; + } port = (host.rdpPort as number) || port || 3389; break; case "vnc": @@ -445,8 +474,13 @@ router.post( username = ""; password = ""; } - const domain = + const storedDomain = (host.rdpDomain as string) || (host.domain as string) || ""; + const domain = resolveRdpDomain( + rdpAuthTypeForConnect, + req.body?.promptedDomain, + storedDomain, + ); // Establish SSH tunnel if jump hosts are configured let jumpHosts: Array<{ hostId: number }> = []; @@ -463,65 +497,72 @@ router.post( if (jumpHosts.length > 0) { try { - const { resolveHostById } = await import("../host-resolver.js"); - const jumpHost = await resolveHostById(jumpHosts[0].hostId, userId); - if (jumpHost) { - const tunnelPort = await new Promise((resolve, reject) => { - const sshClient = new Client(); - sshClient.on("ready", () => { - const server = net.createServer((sock) => { - sshClient.forwardOut( - "127.0.0.1", - 0, - hostname, - port, - (err, stream) => { - if (err) { - sock.destroy(); - return; - } - sock.pipe(stream).pipe(sock); - }, - ); - }); - server.listen(0, "127.0.0.1", () => { - const addr = server.address() as net.AddressInfo; - // Auto-cleanup after 1 hour - setTimeout( - () => { - server.close(); - sshClient.end(); - }, - 60 * 60 * 1000, - ); - resolve(addr.port); - }); - }); - sshClient.on("error", reject); + let guacdUrl: string | undefined; + try { + guacdUrl = + (await createCurrentSettingsRepository().get("guac_url")) ?? + undefined; + } catch { + // Environment/default guacd configuration remains available. + } + const guacdHost = + perConnectionGuacdHost || resolveGuacdOptions(guacdUrl).host; + const tunnelEndpoint = resolveJumpTunnelEndpoint(guacdHost); - const connectOpts: Record = { - host: jumpHost.ip, - port: jumpHost.port || 22, - username: jumpHost.username, - readyTimeout: 30000, - }; - if (jumpHost.key) { - connectOpts.privateKey = jumpHost.key; - if (jumpHost.keyPassword) - connectOpts.passphrase = jumpHost.keyPassword; - } else if (jumpHost.password) { - connectOpts.password = jumpHost.password; - } - sshClient.connect(connectOpts); - }); - hostname = "127.0.0.1"; - port = tunnelPort; - guacLogger.info("SSH tunnel established for guacamole", { - operation: "guac_ssh_tunnel", - hostId, - tunnelPort, + // The chain dials the first hop through that hop's own SOCKS5 + // settings; the target host's proxy config does not apply to it. + const jumpClient = await createJumpHostChain(jumpHosts, userId); + + if (!jumpClient) { + guacLogger.error( + "Failed to establish jump host chain for guacamole", + undefined, + { operation: "guac_ssh_tunnel_error", hostId }, + ); + return res.status(500).json({ + error: "Failed to establish SSH tunnel to remote host", }); } + + const targetHostname = hostname; + const targetPort = port; + const tunnelPort = await new Promise((resolve, reject) => { + const server = net.createServer((sock) => { + jumpClient.forwardOut( + "127.0.0.1", + 0, + targetHostname, + targetPort, + (err, stream) => { + if (err) { + sock.destroy(); + return; + } + sock.pipe(stream).pipe(sock); + }, + ); + }); + server.on("error", reject); + server.listen(0, tunnelEndpoint.bindHost, () => { + const addr = server.address() as net.AddressInfo; + // Auto-cleanup after 1 hour + setTimeout( + () => { + server.close(); + jumpClient.end(); + }, + 60 * 60 * 1000, + ); + resolve(addr.port); + }); + }); + hostname = tunnelEndpoint.advertisedHost; + port = tunnelPort; + guacLogger.info("SSH tunnel established for guacamole", { + operation: "guac_ssh_tunnel", + hostId, + tunnelPort, + }); } catch (tunnelError) { guacLogger.error("Failed to establish SSH tunnel", tunnelError, { operation: "guac_ssh_tunnel_error", @@ -541,7 +582,8 @@ router.post( ? { guacdPort: perConnectionGuacdPort } : {}), }; - const recordingEnabled = host.enableSessionLogging !== false; + const recordingEnabled = + connectionType !== "vnc" && host.enableSessionLogging !== false; const recordingName = `${crypto.randomUUID()}.guac`; const recordingPath = process.env.GUACD_RECORDING_PATH || @@ -553,17 +595,26 @@ router.post( userId, protocol: connectionType as "rdp" | "vnc" | "telnet", path: recordingName, + guacdPath: recordingPath, startedAt: new Date().toISOString(), } : undefined; if (recordingEnabled) { - guacConfig["recording-path"] = recordingPath; - guacConfig["recording-name"] = recordingName; - guacConfig["create-recording-path"] = true; - guacConfig["recording-exclude-output"] = false; - guacConfig["recording-include-keys"] = true; + guacConfig = withRecordingSettings( + guacConfig, + recordingPath, + recordingName, + ); } + const termixConnectId = crypto.randomUUID(); + const termixMeta = { + termixConnectId, + hostId, + ownerUserId: userId, + protocol: connectionType as "rdp" | "vnc" | "telnet", + }; + switch (connectionType) { case "rdp": if (guacConfig["enable-drive"] && !guacConfig["drive-path"]) { @@ -574,23 +625,24 @@ router.post( hostname, username, password, - { + buildRdpSettings({ port, domain, security: (host.rdpSecurity as string) || (host.security as string) || undefined, - "ignore-cert": + ignoreCert: host.rdpIgnoreCert !== undefined ? !!host.rdpIgnoreCert : host.ignoreCert !== undefined ? !!host.ignoreCert : true, - ...guacConfig, - ...guacdOverrides, - }, + guacConfig, + guacdOverrides, + }), recordingMetadata, + termixMeta, ); break; case "vnc": @@ -600,11 +652,11 @@ router.post( password, { port, - security: "any", ...guacConfig, ...guacdOverrides, }, recordingMetadata, + termixMeta, ); break; case "telnet": @@ -618,13 +670,32 @@ router.post( ...guacdOverrides, }, recordingMetadata, + termixMeta, ); break; default: return res.status(400).json({ error: "Invalid connection type" }); } - res.json({ token }); + const sessionInfo = await waitForGuacdOpen(termixConnectId, 10000); + + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: `${connectionType}_connect`, + resourceType: "host", + resourceId: String(hostId), + resourceName: `${hostname}:${port}`, + ipAddress, + userAgent, + success: true, + }); + + res.json({ + token, + guacamoleConnectionId: sessionInfo?.guacamoleConnectionId ?? null, + }); } catch (error) { guacLogger.error("Failed to generate guacamole token for host", error, { operation: "guac_host_token_error", diff --git a/src/backend/hosts/guacamole/token-service.ts b/src/backend/hosts/guacamole/token-service.ts index d2fe23d..8bd34c4 100644 --- a/src/backend/hosts/guacamole/token-service.ts +++ b/src/backend/hosts/guacamole/token-service.ts @@ -2,11 +2,13 @@ import crypto from "crypto"; import { guacLogger } from "../../utils/logger.js"; export interface GuacamoleConnectionSettings { - type: "rdp" | "vnc" | "telnet"; + type?: "rdp" | "vnc" | "telnet"; + join?: string; + readOnly?: boolean; guacdHost?: string; guacdPort?: number; settings: { - hostname: string; + hostname?: string; port?: number; username?: string; password?: string; @@ -28,9 +30,17 @@ export interface GuacamoleConnectionSettings { }; } +export interface TermixGuacMeta { + termixConnectId: string; + hostId: number; + ownerUserId: string; + protocol: "rdp" | "vnc" | "telnet"; +} + export interface GuacamoleToken { connection: GuacamoleConnectionSettings; recording?: GuacamoleRecordingMetadata; + termixMeta?: TermixGuacMeta; } export interface GuacamoleRecordingMetadata { @@ -38,6 +48,9 @@ export interface GuacamoleRecordingMetadata { userId: string; protocol: "rdp" | "vnc" | "telnet"; path: string; + /** Directory guacd was told to write into; differs from the backend's view + * when guacd runs in its own container. */ + guacdPath?: string; startedAt: string; } @@ -137,6 +150,7 @@ export class GuacamoleTokenService { guacdPort?: number; } = {}, recording?: GuacamoleRecordingMetadata, + termixMeta?: TermixGuacMeta, ): string { const { guacdHost, guacdPort, ...settingsOptions } = options; const token: GuacamoleToken = { @@ -155,6 +169,7 @@ export class GuacamoleTokenService { }, }, recording, + termixMeta, }; return this.encryptToken(token); } @@ -168,6 +183,7 @@ export class GuacamoleTokenService { guacdPort?: number; } = {}, recording?: GuacamoleRecordingMetadata, + termixMeta?: TermixGuacMeta, ): string { const { guacdHost, guacdPort, ...settingsOptions } = options; const token: GuacamoleToken = { @@ -184,6 +200,7 @@ export class GuacamoleTokenService { }, }, recording, + termixMeta, }; return this.encryptToken(token); } @@ -197,6 +214,7 @@ export class GuacamoleTokenService { guacdPort?: number; } = {}, recording?: GuacamoleRecordingMetadata, + termixMeta?: TermixGuacMeta, ): string { const { guacdHost, guacdPort, ...settingsOptions } = options; const token: GuacamoleToken = { @@ -213,6 +231,20 @@ export class GuacamoleTokenService { }, }, recording, + termixMeta, + }; + return this.encryptToken(token); + } + + // join tokens never carry recording params - only the primary connection's + // token should write recording-path/recording-name to guacd. + createJoinToken(guacamoleConnectionId: string, readOnly: boolean): string { + const token: GuacamoleToken = { + connection: { + join: guacamoleConnectionId, + readOnly, + settings: {}, + }, }; return this.encryptToken(token); } diff --git a/src/backend/hosts/host-resolver.ts b/src/backend/hosts/host-resolver.ts index 859d8e0..e7739fe 100644 --- a/src/backend/hosts/host-resolver.ts +++ b/src/backend/hosts/host-resolver.ts @@ -1,10 +1,13 @@ +import { getErrorMessage } from "../utils/error-message.js"; import { createCurrentHostResolutionRepository, createCurrentVaultProfileRepository, createCurrentUserRepository, } from "../database/repositories/factory.js"; +import type { HostResolutionHostRecord } from "../database/repositories/host-resolution-repository.js"; import { logAudit } from "../utils/audit-logger.js"; import { logger } from "../utils/logger.js"; +import { resolveRecipientSharedHostAuthentication } from "../utils/shared-host-auth-resolver.js"; import { pickResolvedPassword, pickResolvedUsername, @@ -15,6 +18,33 @@ import type { HostAction } from "../utils/permission-manager.js"; const sshLogger = logger; +/** + * Resolve a host the client named by its sync identity. + * + * `id` is an autoincrement belonging to whichever database produced the row. + * When the desktop app delegates a connection to a sync server, the two + * sequences have no reason to agree, and resolving the client's id here lands + * on whatever host happens to own that number โ€” a different machine, with its + * own address, credentials and host key. `syncId` is the same string on both + * sides, so it names the host the user actually picked. + * + * Returns null when the sync id is unknown here, rather than falling back to + * the numeric id: an unknown host is exactly the case where guessing picks the + * wrong machine. + */ +export async function resolveHostBySyncId( + syncId: string, + userId: string, +): Promise { + const hostId = + await createCurrentHostResolutionRepository().findHostIdBySyncId(syncId); + if (hostId === null) return null; + + // Permissions, decryption, shared-host handling and auditing all belong to + // the id-based path; this only decides which row it is pointed at. + return resolveHostById(hostId, userId); +} + /** * Resolve a host with its credentials server-side by hostId. * This avoids passing credentials through the frontend. @@ -99,6 +129,17 @@ export async function resolveHostById( host.terminalConfig = undefined; } } + if ( + !ownerEquivalent && + host.terminalConfig && + typeof host.terminalConfig === "object" && + !Array.isArray(host.terminalConfig) + ) { + host.terminalConfig = { + ...(host.terminalConfig as Record), + sudoPassword: null, + }; + } if (typeof host.socks5ProxyChain === "string" && host.socks5ProxyChain) { try { host.socks5ProxyChain = JSON.parse(host.socks5ProxyChain as string); @@ -113,44 +154,73 @@ export async function resolveHostById( host.quickActions = []; } } - - if (!ownerEquivalent) { - const resolved = await resolveSharedSshSecrets( - host, - hostId, - userId, - repository, - ); - if (!resolved) return null; - } else if (host.credentialId) { + if (typeof host.portKnockSequence === "string" && host.portKnockSequence) { try { - const cred = (await repository.findCredentialByIdForUser( - host.credentialId as number, - ownerId, - )) as Record | null; + host.portKnockSequence = JSON.parse(host.portKnockSequence as string); + } catch { + host.portKnockSequence = []; + } + } - if (cred) { - host.password = pickResolvedPassword(host.password, cred.password); - // Prefer the normalised private key; fall back to raw key field - host.key = (cred.privateKey || cred.key) as string | null; - host.keyPassword = cred.keyPassword; - host.keyType = cred.keyType; - // CA-signed certificate for cert-based auth - (host as Record).certPublicKey = - cred.certPublicKey || null; - host.username = pickResolvedUsername( - host.username, - cred.username, - host.overrideCredentialUsername, + let sharedAuthResolution: SharedAuthResolution | undefined; + if (!ownerEquivalent) { + sharedAuthResolution = await resolveRecipientSshAuth(host, hostId, userId); + if (!sharedAuthResolution) return null; + } else { + let effectiveCredentialId = host.credentialId as number | null | undefined; + if ( + !effectiveCredentialId && + host.authType === "credential" && + host.folder + ) { + try { + effectiveCredentialId = await repository.findFolderCredentialId( + ownerId, + host.folder as string, ); - host.authType = host.key ? "key" : host.password ? "password" : "none"; + } catch (e) { + sshLogger.warn("Failed to resolve folder credential for host", { + operation: "host_resolver_folder_credential", + hostId, + error: getErrorMessage(e, "Unknown"), + }); + } + } + + if (effectiveCredentialId) { + try { + const cred = (await repository.findCredentialByIdForUser( + effectiveCredentialId, + ownerId, + )) as Record | null; + + if (cred) { + host.password = pickResolvedPassword(host.password, cred.password); + // Prefer the normalised private key; fall back to raw key field + host.key = (cred.privateKey || cred.key) as string | null; + host.keyPassword = cred.keyPassword; + host.keyType = cred.keyType; + // CA-signed certificate for cert-based auth + (host as Record).certPublicKey = + cred.certPublicKey || null; + host.username = pickResolvedUsername( + host.username, + cred.username, + host.overrideCredentialUsername, + ); + host.authType = host.key + ? "key" + : host.password + ? "password" + : "none"; + } + } catch (e) { + sshLogger.warn("Failed to resolve credential for host", { + operation: "host_resolver_credential", + hostId, + error: getErrorMessage(e, "Unknown"), + }); } - } catch (e) { - sshLogger.warn("Failed to resolve credential for host", { - operation: "host_resolver_credential", - hostId, - error: e instanceof Error ? e.message : "Unknown", - }); } } @@ -161,7 +231,7 @@ export async function resolveHostById( // Resolve a Vault SSH signer profile (shared settings, no secrets). The // certificate itself is obtained per-user at connect time via Vault OIDC. - if (host.vaultProfileId) { + if (host.vaultProfileId && sharedAuthResolution !== "recipient-override") { try { const profile = await createCurrentVaultProfileRepository().findById( host.vaultProfileId as number, @@ -174,7 +244,7 @@ export async function resolveHostById( sshLogger.warn("Failed to resolve vault profile for host", { operation: "host_resolver_vault_profile", hostId, - error: e instanceof Error ? e.message : "Unknown", + error: getErrorMessage(e, "Unknown"), }); } } @@ -183,87 +253,78 @@ export async function resolveHostById( } /** - * Fill in SSH auth secrets for a shared (non-owner) requester. Order: - * the recipient's own override credential, then their re-encrypted share - * snapshot. Secret-less auth types (opkssh, vault, agent, none) pass through - * untouched. Returns false when a secret-bearing host has no usable source. + * Resolve SSH auth for a shared (non-owner) requester without exposing the + * owner's password, key, or credential reference. A recipient-owned override + * fully replaces the host auth. An owner-enabled shared snapshot is the + * fallback; otherwise only secret-less auth types pass. */ -async function resolveSharedSshSecrets( +type SharedAuthResolution = + "recipient-override" | "shared-snapshot" | "shared-agent" | "secretless"; + +async function resolveRecipientSshAuth( host: Record, hostId: number, userId: string, - repository: ReturnType, -): Promise { +): Promise { + const ownerAuthHost = { ...host } as HostResolutionHostRecord; + + // The host row is decrypted under its owner's DEK so connection settings are + // available. Remove owner SSH auth before resolving anything for a recipient. + host.password = null; + host.key = null; + host.keyPassword = null; + host.keyType = null; + host.certPublicKey = null; + host.credentialId = null; + try { - const overrideCredId = await repository.findOverrideCredentialId( + const resolution = await resolveRecipientSharedHostAuthentication( + ownerAuthHost, hostId, userId, + "ssh", ); - if (overrideCredId) { - const cred = (await repository.findCredentialByIdForUser( - overrideCredId, - userId, - )) as Record | null; - if (cred) { - host.password = cred.password; - host.key = (cred.privateKey || cred.key) as string | null; - host.keyPassword = cred.keyPassword; - host.keyType = cred.keyType; + + if (resolution.source === "personal-override") { + const credential = resolution.credential; + host.password = credential.password; + host.key = credential.privateKey || credential.key; + host.keyPassword = credential.keyPassword; + host.keyType = credential.keyType; + host.certPublicKey = credential.certPublicKey || null; + host.username = credential.username || host.username; + host.authType = host.key ? "key" : host.password ? "password" : "none"; + return "recipient-override"; + } + + if (resolution.source === "owner-shared") { + if (resolution.authType === "agent") { + return "shared-agent"; + } + const sharedAuth = resolution.secret; + if (sharedAuth) { + host.password = sharedAuth.password || null; + host.key = sharedAuth.key || null; + host.keyPassword = sharedAuth.keyPassword || null; + host.keyType = sharedAuth.keyType || null; host.username = pickResolvedUsername( host.username, - cred.username, + sharedAuth.username, host.overrideCredentialUsername, ); host.authType = host.key ? "key" : host.password ? "password" : "none"; - return true; + return "shared-snapshot"; } } - } catch { - // fall through to the share snapshot - } - try { - const { SharedHostSecretsManager } = - await import("../utils/shared-host-secrets-manager.js"); - const secret = - await SharedHostSecretsManager.getInstance().getSecretForUser( - hostId, - userId, - "ssh", - ); - if (secret) { - host.password = secret.password; - host.key = secret.key; - host.keyPassword = secret.keyPassword; - host.keyType = secret.keyType; - host.username = pickResolvedUsername( - host.username, - secret.username, - host.overrideCredentialUsername, - ); - host.authType = secret.key - ? "key" - : secret.password - ? "password" - : "none"; - return true; + if (resolution.source === "secretless") { + return "secretless"; } - } catch (e) { - sshLogger.warn("Failed to get shared host secret", { - operation: "host_resolver_shared_secret", - hostId, - error: e instanceof Error ? e.message : "Unknown", - }); + } catch { + // A missing/deleted override or snapshot behaves like unavailable auth. } - const needsSecrets = - !!host.credentialId || - host.authType === "password" || - host.authType === "key" || - host.authType === "credential"; - if (!needsSecrets) return true; - - return false; + return null; } /** diff --git a/src/backend/hosts/jump-host-chain.ts b/src/backend/hosts/jump-host-chain.ts index 5821fe7..b744155 100644 --- a/src/backend/hosts/jump-host-chain.ts +++ b/src/backend/hosts/jump-host-chain.ts @@ -1,14 +1,11 @@ import { Client as SSHClient } from "ssh2"; -import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { fileLogger } from "../utils/logger.js"; -import { - createSocks5Connection, - type SOCKS5Config, -} from "../utils/socks5-helper.js"; +import { createSocks5Connection } from "../utils/socks5-helper.js"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { SSHHostKeyVerifier } from "./host-key-verifier.js"; import { getJumpHostSocks5Config } from "./jump-host-proxy.js"; import { applyAgentAuth } from "./terminal-auth-helpers.js"; +import { resolveHostById } from "./host-resolver.js"; type JumpHostConfig = { id: number; @@ -35,64 +32,10 @@ async function resolveJumpHost( userId: string, ): Promise { try { - const repository = createCurrentHostResolutionRepository(); - const ownerId = (await repository.findHostOwnerId(hostId)) ?? userId; - const resolvedHost = await repository.findHostById(hostId, ownerId); - - if (!resolvedHost) { - return null; - } - - const host = resolvedHost as Record; - - if (host.credentialId) { - if (userId !== ownerId) { - try { - const { SharedHostSecretsManager } = - await import("../utils/shared-host-secrets-manager.js"); - const secret = - await SharedHostSecretsManager.getInstance().getSecretForUser( - hostId, - userId, - "ssh", - ); - if (secret) { - return { - ...host, - password: secret.password, - key: secret.key, - keyPassword: secret.keyPassword, - keyType: secret.keyType, - authType: secret.key - ? "key" - : secret.password - ? "password" - : "none", - } as JumpHostConfig; - } - } catch { - // fall through to owner credential lookup - } - } - - const credential = (await repository.findCredentialByIdForUser( - host.credentialId as number, - ownerId, - )) as Record | null; - - if (credential) { - return { - ...host, - password: credential.password as string | undefined, - key: (credential.key || credential.privateKey) as string | undefined, - keyPassword: credential.keyPassword as string | undefined, - keyType: credential.keyType as string | undefined, - authType: credential.authType as string | undefined, - } as JumpHostConfig; - } - } - - return host as JumpHostConfig; + return (await resolveHostById( + hostId, + userId, + )) as unknown as JumpHostConfig | null; } catch (error) { fileLogger.error("Failed to resolve jump host", error, { operation: "resolve_jump_host", @@ -106,7 +49,6 @@ async function resolveJumpHost( export async function createJumpHostChain( jumpHosts: Array<{ hostId: number }>, userId: string, - socks5Config?: SOCKS5Config | null, ): Promise { if (!jumpHosts || jumpHosts.length === 0) { return null; @@ -138,10 +80,7 @@ export async function createJumpHostChain( } } - const firstHopSocks5Config = getJumpHostSocks5Config( - jumpHostConfigs[0], - socks5Config, - ); + const firstHopSocks5Config = getJumpHostSocks5Config(jumpHostConfigs[0]); let proxySocket: import("net").Socket | null = null; if (firstHopSocks5Config?.useSocks5) { const firstHop = jumpHostConfigs[0]!; @@ -263,8 +202,7 @@ export async function createJumpHostChain( const result = await applyAgentAuth( connectConfig, jumpHostConfig.terminalConfig as - | Record - | undefined, + Record | undefined, ); if ("error" in result) { throw new Error(result.error); diff --git a/src/backend/hosts/jump-host-proxy.test.ts b/src/backend/hosts/jump-host-proxy.test.ts new file mode 100644 index 0000000..877998e --- /dev/null +++ b/src/backend/hosts/jump-host-proxy.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { getJumpHostSocks5Config } from "./jump-host-proxy.js"; + +describe("getJumpHostSocks5Config", () => { + it("uses the first jump host proxy settings", () => { + expect( + getJumpHostSocks5Config({ + useSocks5: true, + socks5Host: "proxy.internal", + socks5Port: 1080, + socks5Username: "user", + socks5Password: "secret", + }), + ).toEqual({ + useSocks5: true, + socks5Host: "proxy.internal", + socks5Port: 1080, + socks5Username: "user", + socks5Password: "secret", + socks5ProxyChain: [], + }); + }); + + it("does not use destination proxy settings for the first jump host", () => { + expect(getJumpHostSocks5Config({ useSocks5: false })).toBeNull(); + }); + + it("accepts a serialized proxy chain from the first jump host", () => { + const chain = [ + { + id: "proxy-1", + name: "Proxy 1", + host: "proxy.internal", + port: 1080, + type: "socks5" as const, + }, + ]; + + expect( + getJumpHostSocks5Config({ + useSocks5: true, + socks5ProxyChain: JSON.stringify(chain), + }), + ).toEqual({ + useSocks5: true, + socks5Host: undefined, + socks5Port: undefined, + socks5Username: undefined, + socks5Password: undefined, + socks5ProxyChain: chain, + }); + }); +}); diff --git a/src/backend/hosts/jump-host-proxy.ts b/src/backend/hosts/jump-host-proxy.ts index e16cb74..890446e 100644 --- a/src/backend/hosts/jump-host-proxy.ts +++ b/src/backend/hosts/jump-host-proxy.ts @@ -29,15 +29,14 @@ function parseProxyChain(value: JumpHostProxyConfig["socks5ProxyChain"]) { export function getJumpHostSocks5Config( firstHop: JumpHostProxyConfig | null | undefined, - fallbackConfig?: SOCKS5Config | null, ): SOCKS5Config | null { if (!firstHop?.useSocks5) { - return fallbackConfig ?? null; + return null; } const socks5ProxyChain = parseProxyChain(firstHop.socks5ProxyChain); if (!firstHop.socks5Host && socks5ProxyChain.length === 0) { - return fallbackConfig ?? null; + return null; } return { diff --git a/src/backend/hosts/metrics/alert-engine.ts b/src/backend/hosts/metrics/alert-engine.ts index deface5..8699dce 100644 --- a/src/backend/hosts/metrics/alert-engine.ts +++ b/src/backend/hosts/metrics/alert-engine.ts @@ -5,6 +5,7 @@ import { type AlertPayload, type NotificationChannel, } from "../../utils/notification-sender.js"; +import { sendDiscord } from "../../utils/discord-sender.js"; type AlertTriggerType = | "host_offline" @@ -28,6 +29,24 @@ interface AlertRule { cooldownMinutes: number; } +/** + * Set once the alert rules have been copied into automations. From then on the + * automations engine owns evaluation, and this one stands down rather than + * sending a second notification for every rule. + * + * The class is left in place so an install that has not migrated yet, or one + * rolled back, still alerts exactly as before. + */ +let supersededByAutomations = false; + +export function markAlertEngineSuperseded(): void { + supersededByAutomations = true; +} + +export function isAlertEngineSuperseded(): boolean { + return supersededByAutomations; +} + export class AlertEngine { private static instance: AlertEngine; @@ -55,6 +74,7 @@ export class AlertEngine { disk?: { percent: number | null } | null; }, ): Promise { + if (supersededByAutomations) return; const rules = (await this.loadRulesForHost(hostId)).filter((r) => ["cpu_threshold", "memory_threshold", "disk_threshold"].includes( r.triggerType, @@ -103,6 +123,7 @@ export class AlertEngine { } async evaluateStatus(hostId: number, isOnline: boolean): Promise { + if (supersededByAutomations) return; const currentStatus = isOnline ? "online" : "offline"; const lastStatus = this.lastStatusMap.get(hostId); @@ -138,6 +159,7 @@ export class AlertEngine { ok: boolean, detail?: string, ): Promise { + if (supersededByAutomations) return; const stateKey = `${hostId}:${checkId}`; const lastOk = this.healthCheckStateMap.get(stateKey); this.healthCheckStateMap.set(stateKey, ok); @@ -173,6 +195,7 @@ export class AlertEngine { sshUser: string, fromIp: string, ): Promise { + if (supersededByAutomations) return; const rules = (await this.loadRulesForHostUser(hostId, userId)).filter( (r) => r.triggerType === "user_login", ); @@ -225,7 +248,7 @@ export class AlertEngine { severity: context.severity, }); - repository.pruneFiringsOlderThan(rule.userId, 30); + await repository.pruneFiringsOlderThan(rule.userId, 30); } catch (err) { statsLogger.warn("Failed to write alert firing", { operation: "alert_firing_insert_error", @@ -236,13 +259,42 @@ export class AlertEngine { const channels = await this.loadChannelsForRule(rule.id); for (const channel of channels) { - sendNotification(channel, payload).catch((err) => { - statsLogger.warn("Failed to send notification", { - operation: "notification_delivery_error", - channelId: channel.id, - error: err instanceof Error ? err.message : String(err), + if (channel.type === "discord") { + // parse config and send via Discord sender directly + try { + let parsed: Record; + try { + parsed = JSON.parse(channel.config) as Record; + } catch { + statsLogger.warn("Failed to parse discord channel config", { + operation: "discord_config_parse_error", + channelId: channel.id, + }); + continue; + } + sendDiscord(parsed as any, payload).catch((err) => { + statsLogger.warn("Failed to send discord notification", { + operation: "discord_notification_error", + channelId: channel.id, + error: err instanceof Error ? err.message : String(err), + }); + }); + } catch (err) { + statsLogger.warn("Failed to send discord notification", { + operation: "discord_notification_error", + channelId: channel.id, + error: err instanceof Error ? err.message : String(err), + }); + } + } else { + sendNotification(channel, payload).catch((err) => { + statsLogger.warn("Failed to send notification", { + operation: "notification_delivery_error", + channelId: channel.id, + error: err instanceof Error ? err.message : String(err), + }); }); - }); + } } } diff --git a/src/backend/hosts/metrics/automation-bridge.ts b/src/backend/hosts/metrics/automation-bridge.ts new file mode 100644 index 0000000..a1474e7 --- /dev/null +++ b/src/backend/hosts/metrics/automation-bridge.ts @@ -0,0 +1,62 @@ +/** + * One-line hand-off from the metrics poller to the automations engine. + * + * The poller is already a very large module, so the hooks it calls live here + * instead. Everything is fire-and-forget and imported lazily: a failure in the + * automations layer must never disturb metric collection, and a static import + * would create a cycle (automations reads repositories, which the metrics + * module also pulls in). + */ + +import type { MetricsSnapshot } from "../../automations/conditions.js"; + +export function notifyAutomationMetrics( + hostId: number, + ownerUserId: string, + metrics: MetricsSnapshot, +): void { + if (!ownerUserId) return; + import("../../automations/triggers.js") + .then((triggers) => triggers.onMetrics({ hostId, ownerUserId, metrics })) + .catch(() => {}); +} + +export function notifyAutomationStatus( + hostId: number, + ownerUserId: string, + online: boolean, +): void { + if (!ownerUserId) return; + import("../../automations/triggers.js") + .then((triggers) => triggers.onStatus({ hostId, ownerUserId, online })) + .catch(() => {}); +} + +export function notifyAutomationHealthCheck( + hostId: number, + userId: string, + checkId: string, + ok: boolean, + detail?: string, +): void { + if (!userId) return; + import("../../automations/triggers.js") + .then((triggers) => + triggers.onHealthCheck({ hostId, userId, checkId, ok, detail }), + ) + .catch(() => {}); +} + +export function notifyAutomationInternalEvent( + event: string, + userId: string, + hostId?: number, + details?: Record, +): void { + if (!userId) return; + import("../../automations/triggers.js") + .then((triggers) => + triggers.onInternalEvent({ event, userId, hostId, details }), + ) + .catch(() => {}); +} diff --git a/src/backend/hosts/metrics/helpers.ts b/src/backend/hosts/metrics/helpers.ts index 6760cfb..bd9eb4c 100644 --- a/src/backend/hosts/metrics/helpers.ts +++ b/src/backend/hosts/metrics/helpers.ts @@ -21,6 +21,18 @@ export function isTcpPingEnabled(statsConfig: TcpPingStatsConfig): boolean { return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing; } +export function parseStatusHostIds(value: unknown): Set | null { + if (value === undefined) return null; + if (typeof value !== "string") return new Set(); + + return new Set( + value + .split(",") + .map(Number) + .filter((id) => Number.isSafeInteger(id) && id > 0), + ); +} + export function tcpPingThroughJumpHost( jumpClient: Pick, host: string, diff --git a/src/backend/hosts/metrics/host-status.test.ts b/src/backend/hosts/metrics/host-status.test.ts new file mode 100644 index 0000000..f41c454 --- /dev/null +++ b/src/backend/hosts/metrics/host-status.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { + statusAfterAuthentication, + statusAfterReachabilityCheck, +} from "./host-status.js"; + +describe("host availability status", () => { + it("does not call a TCP-reachable host online before authentication", () => { + expect(statusAfterReachabilityCheck(true)).toBe("reachable"); + }); + + it("keeps a verified host online across later reachability checks", () => { + expect(statusAfterReachabilityCheck(true, "online")).toBe("online"); + }); + + it("marks successful authentication online", () => { + expect(statusAfterAuthentication(true, "reachable")).toBe("online"); + }); + + it("downgrades failed authentication without hiding reachability", () => { + expect(statusAfterAuthentication(false, "online")).toBe("reachable"); + expect(statusAfterAuthentication(false, "offline")).toBe("offline"); + }); +}); diff --git a/src/backend/hosts/metrics/host-status.ts b/src/backend/hosts/metrics/host-status.ts new file mode 100644 index 0000000..3c5b391 --- /dev/null +++ b/src/backend/hosts/metrics/host-status.ts @@ -0,0 +1,17 @@ +export type HostStatus = "online" | "reachable" | "offline"; + +export function statusAfterReachabilityCheck( + reachable: boolean, + current?: HostStatus, +): HostStatus { + if (!reachable) return "offline"; + return current === "online" ? "online" : "reachable"; +} + +export function statusAfterAuthentication( + authenticated: boolean, + current?: HostStatus, +): HostStatus { + if (authenticated) return "online"; + return current === "offline" ? "offline" : "reachable"; +} diff --git a/src/backend/hosts/metrics/index.ts b/src/backend/hosts/metrics/index.ts index b934a34..d04f038 100644 --- a/src/backend/hosts/metrics/index.ts +++ b/src/backend/hosts/metrics/index.ts @@ -1,6 +1,8 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import express from "express"; import net from "net"; import { createCorsMiddleware } from "../../utils/cors-config.js"; +import { createCompressionMiddleware } from "../../utils/compression-config.js"; import cookieParser from "cookie-parser"; import { Client, type ConnectConfig } from "ssh2"; import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js"; @@ -24,7 +26,10 @@ import type { } from "../../../types/connection-log.js"; import { collectCpuMetrics } from "./widgets/cpu-collector.js"; import { collectMemoryMetrics } from "./widgets/memory-collector.js"; -import { collectDiskMetrics } from "./widgets/disk-collector.js"; +import { + collectDiskMetrics, + type DiskFilesystem, +} from "./widgets/disk-collector.js"; import { collectNetworkMetrics } from "./widgets/network-collector.js"; import { collectUptimeMetrics } from "./widgets/uptime-collector.js"; import { collectProcessesMetrics } from "./widgets/processes-collector.js"; @@ -32,6 +37,7 @@ import { collectSystemMetrics } from "./widgets/system-collector.js"; import { collectLoginStats } from "./widgets/login-stats-collector.js"; import { collectPortsMetrics } from "./widgets/ports-collector.js"; import { collectFirewallMetrics } from "./widgets/firewall-collector.js"; +import { collectTemperatureMetrics } from "./widgets/temperature-collector.js"; import { createSocks5Connection, type SOCKS5Config, @@ -43,17 +49,31 @@ import { registerHostMetricsSettingsRoutes } from "./settings-routes.js"; import { registerHostMetricsViewerRoutes } from "./viewer-routes.js"; import { registerHostMetricsPreferencesRoutes } from "./preferences-routes.js"; import { registerHostMetricsHistoryRoutes } from "./history-routes.js"; +import { registerProxmoxStatsRoutes } from "./proxmox-stats-routes.js"; +import { registerProxmoxStatsHistoryRoutes } from "./proxmox-stats-history-routes.js"; +import { ProxmoxPollingManager } from "./proxmox-stats-polling.js"; import { AlertEngine } from "./alert-engine.js"; +import { + notifyAutomationMetrics, + notifyAutomationStatus, +} from "./automation-bridge.js"; import { registerManagerRoutes } from "./managers/index.js"; import { resolveSshConnectConfigHost } from "../ssh-dns.js"; import { AccessDeniedError } from "./managers/route-helpers.js"; import type { ManagerHost } from "./managers/types.js"; import { createJumpHostChain } from "../jump-host-chain.js"; +import { resolveHostById } from "../host-resolver.js"; import { isTcpPingEnabled, + parseStatusHostIds, supportsMetrics, tcpPingThroughJumpHost, } from "./helpers.js"; +import { + type HostStatus, + statusAfterAuthentication, + statusAfterReachabilityCheck, +} from "./host-status.js"; import { createConnectionLog } from "../connection-log.js"; import { cleanupMetricsSession, @@ -65,9 +85,12 @@ import { } from "./sessions.js"; import { authFailureTracker, + canStartInitialMetrics, hostPollCache, + initialMetricsPollLimiter, metricsCache, metricsPollLimiter, + metricsConcurrencyFor, pollingBackoff, requestQueue, statusPollLimiter, @@ -76,8 +99,6 @@ import { const authManager = AuthManager.getInstance(); const permissionManager = PermissionManager.getInstance(); -type HostStatus = "online" | "offline"; - interface SSHHostWithCredentials { id: number; name: string; @@ -101,6 +122,8 @@ interface SSHHostWithCredentials { tunnelConnections: unknown[]; jumpHosts?: Array<{ hostId: number }>; statsConfig?: string | StatsConfig; + enableProxmoxStats?: boolean; + proxmoxStatsConfig?: unknown; createdAt: string; updatedAt: string; userId: string; @@ -132,6 +155,8 @@ interface StatsConfig { metricsInterval: number; useGlobalMetricsInterval?: boolean; disableTcpPing?: boolean; + excludedMounts?: string[]; + monitoredMounts?: Array<{ path: string; label?: string }>; } const DEFAULT_STATS_CONFIG: StatsConfig = { @@ -146,11 +171,13 @@ const DEFAULT_STATS_CONFIG: StatsConfig = { "processes", "ports", "firewall", + "temperature", ], statusCheckEnabled: true, statusCheckInterval: 60, metricsEnabled: true, metricsInterval: 30, + excludedMounts: [], }; interface HostPollingConfig { @@ -177,6 +204,7 @@ class PollingManager { /** Skip stacking another status/metrics poll while one is already running. */ private statusInFlight = new Set(); private metricsInFlight = new Set(); + private initialMetricsRequested = new Set(); constructor() { this.viewerCleanupInterval = setInterval(() => { @@ -184,6 +212,30 @@ class PollingManager { }, 60000); } + /** + * Keeps poll concurrency matched to how many hosts are actually being + * polled, so a sweep still finishes inside its interval as a fleet grows. + */ + private syncPollConcurrency(): void { + const metricsHosts = Array.from(this.pollingConfigs.values()).filter( + (config) => config.statsConfig.metricsEnabled, + ).length; + + const target = metricsConcurrencyFor(metricsHosts); + if (target === metricsPollLimiter.limit) return; + + const previous = metricsPollLimiter.limit; + metricsPollLimiter.setLimit(target); + statsLogger.info( + `Metrics poll concurrency ${previous} -> ${target} for ${metricsHosts} host(s)`, + { + operation: "metrics_concurrency_resize", + hosts: metricsHosts, + concurrency: target, + }, + ); + } + /** Spread timers so N hosts do not fire on the same wall-clock second. */ private intervalWithJitter(intervalMs: number, hostId: number): number { const spread = Math.min(intervalMs * 0.2, 15_000); @@ -259,6 +311,54 @@ class PollingManager { }); } + private scheduleInitialMetricsPoll( + host: SSHHostWithCredentials, + viewerUserId?: string, + ): void { + if ( + this.metricsStore.has(host.id) || + this.initialMetricsRequested.has(host.id) + ) { + return; + } + + this.initialMetricsRequested.add(host.id); + void initialMetricsPollLimiter + .run(async () => { + if ( + !canStartInitialMetrics( + this.statusStore.get(host.id)?.status, + this.activeViewers.has(host.id), + isTcpPingEnabled( + this.pollingConfigs.get(host.id)?.statsConfig ?? + this.parseStatsConfig(host.statsConfig), + ), + ) + ) { + return; + } + if (this.metricsInFlight.has(host.id)) return; + + this.metricsInFlight.add(host.id); + try { + await metricsPollLimiter.run(() => + this.pollHostMetrics(host, viewerUserId), + ); + } finally { + this.metricsInFlight.delete(host.id); + } + }) + .catch((err) => { + statsLogger.error("Initial metrics polling failed", err, { + operation: "initial_metrics_poll_unhandled", + hostId: host.id, + }); + }) + .finally(() => { + this.initialMetricsRequested.delete(host.id); + }); + } + private getGlobalDefaults(): { statusCheckInterval: number; metricsInterval: number; @@ -306,7 +406,7 @@ class PollingManager { parsed = temp as StatsConfig; } catch (error) { statsLogger.warn( - `Failed to parse statsConfig: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to parse statsConfig: ${getErrorMessage(error)}`, { operation: "parse_stats_config_error", statsConfigStr, @@ -370,6 +470,7 @@ class PollingManager { if (!isTcpPingEnabled(statsConfig) && !statsConfig.metricsEnabled) { this.pollingConfigs.delete(host.id); + this.syncPollConcurrency(); this.statusStore.delete(host.id); this.metricsStore.delete(host.id); return; @@ -380,6 +481,7 @@ class PollingManager { statsConfig, viewerUserId, }; + this.pollingConfigs.set(host.id, config); if (isTcpPingEnabled(statsConfig)) { const intervalMs = this.intervalWithJitter( @@ -405,21 +507,18 @@ class PollingManager { host.id, ); - // First sample still awaited (gated) so callers can rely on a warm cache. - if (!this.metricsInFlight.has(host.id)) { - this.metricsInFlight.add(host.id); - try { - await metricsPollLimiter.run(() => - this.pollHostMetrics(host, viewerUserId), - ); - } catch (err) { - statsLogger.error("Metrics polling failed", err, { - operation: "metrics_poll_unhandled", - hostId: host.id, - }); - } finally { - this.metricsInFlight.delete(host.id); - } + // Viewer registration can arrive for an entire fleet at once. Only + // collect the first heavy SSH sample after the cheap status probe has + // confirmed the host is reachable; the status completion path below + // starts it when the result was not already cached. + if ( + canStartInitialMetrics( + this.statusStore.get(host.id)?.status, + this.activeViewers.has(host.id), + isTcpPingEnabled(statsConfig), + ) + ) { + this.scheduleInitialMetricsPoll(host, viewerUserId); } config.metricsTimer = setInterval(() => { @@ -439,7 +538,7 @@ class PollingManager { this.metricsStore.delete(host.id); } - this.pollingConfigs.set(host.id, config); + this.syncPollConcurrency(); } private async pollHostStatus( @@ -462,24 +561,9 @@ class PollingManager { let isOnline: boolean; if (refreshedHost.jumpHosts && refreshedHost.jumpHosts.length > 0) { - const proxyConfig: SOCKS5Config | null = - refreshedHost.useSocks5 && - (refreshedHost.socks5Host || - (refreshedHost.socks5ProxyChain && - refreshedHost.socks5ProxyChain.length > 0)) - ? { - useSocks5: true, - socks5Host: refreshedHost.socks5Host, - socks5Port: refreshedHost.socks5Port, - socks5Username: refreshedHost.socks5Username, - socks5Password: refreshedHost.socks5Password, - socks5ProxyChain: refreshedHost.socks5ProxyChain, - } - : null; const jumpClient = await createJumpHostChain( refreshedHost.jumpHosts, userId, - proxyConfig, ); isOnline = jumpClient ? await tcpPingThroughJumpHost( @@ -493,13 +577,23 @@ class PollingManager { isOnline = await tcpPing(refreshedHost.ip, pingPort, 5000); } const statusEntry: StatusEntry = { - status: isOnline ? "online" : "offline", + status: statusAfterReachabilityCheck( + isOnline, + this.statusStore.get(refreshedHost.id)?.status, + ), lastChecked: new Date().toISOString(), }; this.statusStore.set(refreshedHost.id, statusEntry); + if (isOnline && this.activeViewers.has(refreshedHost.id)) { + const config = this.pollingConfigs.get(refreshedHost.id); + if (config?.statsConfig.metricsEnabled) { + this.scheduleInitialMetricsPoll(config.host, config.viewerUserId); + } + } AlertEngine.getInstance() .evaluateStatus(refreshedHost.id, isOnline) .catch(() => {}); + notifyAutomationStatus(refreshedHost.id, refreshedHost.userId, isOnline); } catch { const statusEntry: StatusEntry = { status: "offline", @@ -509,6 +603,7 @@ class PollingManager { AlertEngine.getInstance() .evaluateStatus(refreshedHost.id, false) .catch(() => {}); + notifyAutomationStatus(refreshedHost.id, refreshedHost.userId, false); } } @@ -544,8 +639,19 @@ class PollingManager { return; } + let authenticated = false; try { - const metrics = await collectMetrics(refreshedHost); + const metrics = await collectMetrics(refreshedHost, () => { + authenticated = true; + this.statusStore.set(refreshedHost.id, { + status: statusAfterAuthentication(true), + lastChecked: new Date().toISOString(), + }); + }); + this.statusStore.set(refreshedHost.id, { + status: statusAfterAuthentication(true), + lastChecked: new Date().toISOString(), + }); this.metricsStore.set(refreshedHost.id, { data: metrics, timestamp: Date.now(), @@ -554,9 +660,19 @@ class PollingManager { AlertEngine.getInstance() .evaluateMetrics(refreshedHost.id, metrics) .catch(() => {}); + notifyAutomationMetrics(refreshedHost.id, refreshedHost.userId, metrics); pollingBackoff.reset(refreshedHost.id); authFailureTracker.reset(refreshedHost.id); } catch (error) { + if (!authenticated) { + this.statusStore.set(refreshedHost.id, { + status: statusAfterAuthentication( + false, + this.statusStore.get(refreshedHost.id)?.status, + ), + lastChecked: new Date().toISOString(), + }); + } const isAuthError = error instanceof Error && (error.message.includes("authentication") || @@ -629,7 +745,7 @@ class PollingManager { }); const retentionDays = this.getRetentionDays(); - repository.pruneOlderThan(hostId, retentionDays); + await repository.pruneOlderThan(hostId, retentionDays); } catch (err) { statsLogger.warn("Failed to write metrics history", { operation: "insert_metrics_history", @@ -652,6 +768,8 @@ class PollingManager { } this.pollingConfigs.delete(hostId); + + this.syncPollConcurrency(); if (clearData) { this.statusStore.delete(hostId); this.metricsStore.delete(hostId); @@ -694,6 +812,24 @@ class PollingManager { } } + async reconcileStatusPolling( + userId: string, + allowedHostIds: Set, + ): Promise { + const hosts = await fetchAllHosts(userId); + + for (const host of hosts) { + if (allowedHostIds.has(host.id)) { + if (!this.pollingConfigs.has(host.id)) { + await this.startPollingForHost(host, { statusOnly: true }); + } + continue; + } + + this.stopPollingForHost(host.id, true); + } + } + async refreshHostPolling(userId: string): Promise { const hosts = await fetchAllHosts(userId); const currentHostIds = new Set(hosts.map((h) => h.id)); @@ -723,7 +859,11 @@ class PollingManager { for (const [hostId, config] of this.pollingConfigs.entries()) { const status = this.statusStore.get(hostId); - if (!status || status.status === "online") { + if ( + !status || + status.status === "online" || + status.status === "reachable" + ) { hostsToRefresh.push({ host: config.host, viewerUserId: config.viewerUserId, @@ -847,6 +987,7 @@ function validateHostId( } const app = express(); +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware()); app.use(cookieParser()); app.use(express.json({ limit: "1mb" })); @@ -855,6 +996,38 @@ app.use((_req, res, next) => { next(); }); +// Internal endpoint โ€” only accepts calls from localhost. Registered before +// the auth middleware since it's a service-to-service call authenticated by +// IP + shared secret, not a user JWT. +// Used by the main backend to notify the metrics service of SSH login events. +app.post("/internal/login-alert", async (req, res) => { + const remoteIp = req.socket.remoteAddress; + if ( + remoteIp !== "127.0.0.1" && + remoteIp !== "::1" && + remoteIp !== "::ffff:127.0.0.1" + ) { + return res.status(403).json({ error: "Forbidden" }); + } + const systemCrypto = (await import("../../utils/system-crypto.js")) + .SystemCrypto; + const expectedToken = await systemCrypto.getInstance().getInternalAuthToken(); + const token = req.headers["x-internal-auth"]; + if (!token || token !== expectedToken) { + return res.status(403).json({ error: "Forbidden" }); + } + const { hostId, userId, sshUser, fromIp } = req.body as { + hostId: number; + userId: string; + sshUser: string; + fromIp: string; + }; + AlertEngine.getInstance() + .evaluateUserLogin(hostId, userId, sshUser, fromIp) + .catch(() => {}); + res.json({ ok: true }); +}); + app.use(authManager.createAuthMiddleware()); const requireAdmin = authManager.createAdminMiddleware(); @@ -874,7 +1047,7 @@ async function fetchAllHosts( } } catch (err) { statsLogger.warn( - `Failed to resolve credentials for host ${host.id}: ${err instanceof Error ? err.message : "Unknown error"}`, + `Failed to resolve credentials for host ${host.id}: ${getErrorMessage(err)}`, ); } } @@ -895,14 +1068,9 @@ async function fetchHostById( return undefined; } - const accessInfo = await permissionManager.canAccessHost( - userId, - id, - "connect", - ); - - if (!accessInfo.hasAccess) { - statsLogger.warn(`User ${userId} cannot access host ${id}`, { + const host = await resolveHostById(id, userId); + if (!host) { + statsLogger.warn(`User ${userId} cannot resolve host ${id}`, { operation: "fetch_host_access_denied", userId, hostId: id, @@ -910,14 +1078,7 @@ async function fetchHostById( return undefined; } - const repository = createCurrentHostResolutionRepository(); - const host = await repository.findHostById(id, userId); - - if (!host) { - return undefined; - } - - return await resolveHostCredentials(host, userId); + return host as SSHHostWithCredentials; } catch (err) { statsLogger.error(`Failed to fetch host ${id}`, err); return undefined; @@ -1078,7 +1239,7 @@ async function resolveHostCredentials( } } catch (error) { statsLogger.warn( - `Failed to resolve credential ${host.credentialId} for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to resolve credential ${host.credentialId} for host ${host.id}: ${getErrorMessage(error)}`, ); addLegacyCredentials(baseHost, host); if (baseHost.authType === "credential") { @@ -1096,7 +1257,7 @@ async function resolveHostCredentials( return baseHost as unknown as SSHHostWithCredentials; } catch (error) { statsLogger.error( - `Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to resolve host credentials for host ${host.id}: ${getErrorMessage(error)}`, ); return undefined; } @@ -1202,7 +1363,7 @@ async function buildSshConfig( } } catch (keyError) { statsLogger.error( - `SSH key format error for host ${host.ip}: ${keyError instanceof Error ? keyError.message : "Unknown error"}`, + `SSH key format error for host ${host.ip}: ${getErrorMessage(keyError)}`, ); throw new Error(`Invalid SSH key format for host ${host.ip}`, { cause: keyError, @@ -1293,11 +1454,7 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise { let jumpClient: Client | null = null; if (hasJumpHosts) { - jumpClient = await createJumpHostChain( - host.jumpHosts!, - host.userId!, - proxyConfig, - ); + jumpClient = await createJumpHostChain(host.jumpHosts!, host.userId!); if (!jumpClient) { throw new Error("Failed to establish jump host chain"); @@ -1314,10 +1471,7 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise { } } catch (proxyError) { throw new Error( - "Proxy connection failed: " + - (proxyError instanceof Error - ? proxyError.message - : "Unknown error"), + "Proxy connection failed: " + getErrorMessage(proxyError), { cause: proxyError }, ); } @@ -1443,7 +1597,17 @@ async function withSshConnection( return withConnection(key, factory, fn); } -async function collectMetrics(host: SSHHostWithCredentials): Promise<{ +const proxmoxPollingManager = new ProxmoxPollingManager( + { + fetchHostById, + withSshConnection, + }, +); + +async function collectMetrics( + host: SSHHostWithCredentials, + onAuthenticated?: () => void, +): Promise<{ cpu: { percent: number | null; cores: number | null; @@ -1459,6 +1623,8 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ usedHuman: string | null; totalHuman: string | null; availableHuman: string | null; + mount: string | null; + filesystems: DiskFilesystem[]; }; network: { interfaces: Array<{ @@ -1467,6 +1633,8 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ state: string; rxBytes: string | null; txBytes: string | null; + rxRateBps: number | null; + txRateBps: number | null; }>; }; uptime: { @@ -1501,6 +1669,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ const cached = metricsCache.get(host.id); if (cached) { + onAuthenticated?.(); return cached as ReturnType extends Promise ? T : never; @@ -1511,10 +1680,22 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ const existingSession = metricsSessions[sessionKey]; try { + const excludedMounts = pollingManager.parseStatsConfig( + host.statsConfig, + ).excludedMounts; + const monitoredMounts = pollingManager.parseStatsConfig( + host.statsConfig, + ).monitoredMounts; + const collectFn = async (client: Client) => { + onAuthenticated?.(); const cpu = await collectCpuMetrics(client); const memory = await collectMemoryMetrics(client); - const disk = await collectDiskMetrics(client); + const disk = await collectDiskMetrics( + client, + excludedMounts, + monitoredMounts, + ); const network = await collectNetworkMetrics(client); const uptime = await collectUptimeMetrics(client); const processes = await collectProcessesMetrics(client); @@ -1582,6 +1763,21 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ // expected } + let temperature: { + source: "sysfs" | "sensors" | "none"; + highestCelsius: number | null; + sensors: Array<{ label: string; celsius: number }>; + } = { + source: "none", + highestCelsius: null, + sensors: [], + }; + try { + temperature = await collectTemperatureMetrics(client); + } catch { + // expected + } + const result = { cpu, memory, @@ -1593,6 +1789,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ login_stats, ports, firewall, + temperature, }; metricsCache.set(host.id, result); @@ -1724,15 +1921,28 @@ app.get("/status", async (req, res) => { }); } - const statuses = pollingManager.getAllStatuses(); - if (statuses.size === 0) { + const requestedHostIds = parseStatusHostIds(req.query.hostIds); + if (requestedHostIds !== null) { + await pollingManager.reconcileStatusPolling(userId, requestedHostIds); + } else if (pollingManager.getAllStatuses().size === 0) { await pollingManager.initializePolling(userId); } + // One batched permission resolution for the whole fleet; this endpoint is + // polled every few seconds, and a per-host check made it linear in host + // count against the database. + const entries = Array.from(pollingManager.getAllStatuses().entries()); + const allowed = await permissionManager.filterAccessibleHostIds( + userId, + entries.map(([id]) => id), + ); + const result: Record = {}; - for (const [id, entry] of pollingManager.getAllStatuses().entries()) { - const access = await permissionManager.canAccessHost(userId, id, "connect"); - if (access.hasAccess) { + for (const [id, entry] of entries) { + if ( + allowed.has(id) && + (requestedHostIds === null || requestedHostIds.has(id)) + ) { result[id] = entry; } } @@ -2010,6 +2220,8 @@ app.get("/metrics/:id", validateHostId, async (req, res) => { usedHuman: null, totalHuman: null, availableHuman: null, + mount: null, + filesystems: [], }, network: { interfaces: [] }, uptime: { seconds: null, formatted: null }, @@ -2393,7 +2605,7 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { createConnectionLog( "error", "proxy", - `Jump host connection failed: ${error instanceof Error ? error.message : "Unknown error"}`, + `Jump host connection failed: ${getErrorMessage(error)}`, ), ); reject(error); @@ -2429,7 +2641,7 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { createConnectionLog( "error", "proxy", - `SOCKS5 proxy connection failed: ${error instanceof Error ? error.message : "Unknown error"}`, + `SOCKS5 proxy connection failed: ${getErrorMessage(error)}`, ), ); reject(error); @@ -2462,14 +2674,11 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { createConnectionLog( "error", "stats_connecting", - `Failed to start metrics: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to start metrics: ${getErrorMessage(error)}`, ), ); res.status(500).json({ - error: - error instanceof Error - ? error.message - : "Failed to start metrics collection", + error: getErrorMessage(error, "Failed to start metrics collection"), connectionLogs, }); } @@ -2540,10 +2749,7 @@ app.post("/metrics/stop/:id", validateHostId, async (req, res) => { error: error instanceof Error ? error.message : String(error), }); res.status(500).json({ - error: - error instanceof Error - ? error.message - : "Failed to stop metrics collection", + error: getErrorMessage(error, "Failed to stop metrics collection"), }); } }); @@ -2720,6 +2926,20 @@ app.post("/metrics/connect-totp", async (req, res) => { } }); +// Lets automations keep metrics flowing for hosts they watch, through the same +// viewer refcount the UI uses rather than around it. +import("../../automations/headless-viewer.js") + .then(({ setViewerRegistry }) => { + setViewerRegistry({ + registerViewer: (hostId, sessionId, userId) => + pollingManager.registerViewer(hostId, sessionId, userId), + unregisterViewer: (hostId, sessionId) => + pollingManager.unregisterViewer(hostId, sessionId), + updateHeartbeat: (sessionId) => pollingManager.updateHeartbeat(sessionId), + }); + }) + .catch(() => {}); + registerHostMetricsViewerRoutes(app, { fetchHostById, supportsMetrics: (host: SSHHostWithCredentials) => supportsMetrics(host), @@ -2757,6 +2977,40 @@ registerHostMetricsHistoryRoutes(app, { (await permissionManager.canAccessHost(userId, hostId, level)).hasAccess, }); +registerHostMetricsViewerRoutes< + SSHHostWithCredentials, + { metricsEnabled: boolean } +>(app, { + fetchHostById, + // Proxmox Stats viewers are gated by enableProxmoxStats + host-type support, + // not the Host Metrics statsConfig - fold both checks in here since + // supportsMetrics receives the full host, unlike parseStatsConfig below. + supportsMetrics: (host: SSHHostWithCredentials) => + supportsMetrics(host) && host.enableProxmoxStats === true, + parseStatsConfig: () => ({ metricsEnabled: true }), + updateHeartbeat: (viewerSessionId) => + proxmoxPollingManager.updateHeartbeat(viewerSessionId), + registerViewer: (hostId, viewerSessionId, userId) => + proxmoxPollingManager.registerViewer(hostId, viewerSessionId, userId), + unregisterViewer: (hostId, viewerSessionId) => + proxmoxPollingManager.unregisterViewer(hostId, viewerSessionId), + pathPrefix: "proxmox-stats", +}); + +registerProxmoxStatsRoutes(app, { + validateHostId, + fetchHostById, + canAccessHost: async (userId, hostId, level) => + (await permissionManager.canAccessHost(userId, hostId, level)).hasAccess, + pollingManager: proxmoxPollingManager, +}); + +registerProxmoxStatsHistoryRoutes(app, { + validateHostId, + canAccessHost: async (userId, hostId, level) => + (await permissionManager.canAccessHost(userId, hostId, level)).hasAccess, +}); + registerManagerRoutes(app, { validateHostId, runOnHost: async (hostId, userId, level, fn) => { @@ -2785,44 +3039,16 @@ registerManagerRoutes(app, { }, }); -// Internal endpoint โ€” only accepts calls from localhost. -// Used by the main backend to notify the metrics service of SSH login events. -app.post("/internal/login-alert", async (req, res) => { - const remoteIp = req.socket.remoteAddress; - if ( - remoteIp !== "127.0.0.1" && - remoteIp !== "::1" && - remoteIp !== "::ffff:127.0.0.1" - ) { - return res.status(403).json({ error: "Forbidden" }); - } - const systemCrypto = (await import("../../utils/system-crypto.js")) - .SystemCrypto; - const expectedToken = await systemCrypto.getInstance().getInternalAuthToken(); - const token = req.headers["x-internal-auth"]; - if (!token || token !== expectedToken) { - return res.status(403).json({ error: "Forbidden" }); - } - const { hostId, userId, sshUser, fromIp } = req.body as { - hostId: number; - userId: string; - sshUser: string; - fromIp: string; - }; - AlertEngine.getInstance() - .evaluateUserLogin(hostId, userId, sshUser, fromIp) - .catch(() => {}); - res.json({ ok: true }); -}); - process.on("SIGINT", () => { pollingManager.destroy(); + proxmoxPollingManager.destroy(); connectionPool.destroy(); process.exit(0); }); process.on("SIGTERM", () => { pollingManager.destroy(); + proxmoxPollingManager.destroy(); connectionPool.destroy(); process.exit(0); }); diff --git a/src/backend/hosts/metrics/managers/health.ts b/src/backend/hosts/metrics/managers/health.ts index f84928a..6c8abf7 100644 --- a/src/backend/hosts/metrics/managers/health.ts +++ b/src/backend/hosts/metrics/managers/health.ts @@ -8,6 +8,7 @@ import { shellSingleQuote } from "./exec-elevated.js"; import { isValidPort } from "./validation.js"; import type { ManagerRoutesDeps } from "./types.js"; import { AlertEngine } from "../alert-engine.js"; +import { notifyAutomationHealthCheck } from "../automation-bridge.js"; export interface HealthCheck { id: string; @@ -165,6 +166,13 @@ export function registerHealthRoutes( r.detail ?? undefined, ) .catch(() => {}); + notifyAutomationHealthCheck( + host.id, + userId, + r.checkId, + r.ok, + r.detail ?? undefined, + ); } } @@ -244,6 +252,13 @@ export function registerHealthRoutes( r.detail ?? undefined, ) .catch(() => {}); + notifyAutomationHealthCheck( + host.id, + userId, + r.checkId, + r.ok, + r.detail ?? undefined, + ); } } return { results }; diff --git a/src/backend/hosts/metrics/managers/logs.ts b/src/backend/hosts/metrics/managers/logs.ts index 6051726..c1e88ef 100644 --- a/src/backend/hosts/metrics/managers/logs.ts +++ b/src/backend/hosts/metrics/managers/logs.ts @@ -1,8 +1,7 @@ import type { Express } from "express"; import { execCommand } from "../widgets/common-utils.js"; -import { execElevated } from "./exec-elevated.js"; +import { execElevated, shellSingleQuote } from "./exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { shellSingleQuote } from "./exec-elevated.js"; import { isAllowedPath, isValidSystemdUnit } from "./validation.js"; import type { ManagerRoutesDeps } from "./types.js"; diff --git a/src/backend/hosts/metrics/managers/route-helpers.ts b/src/backend/hosts/metrics/managers/route-helpers.ts index 03a11c4..1de07c6 100644 --- a/src/backend/hosts/metrics/managers/route-helpers.ts +++ b/src/backend/hosts/metrics/managers/route-helpers.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../../utils/error-message.js"; import type { Request, Response } from "express"; import type { Client } from "ssh2"; import type { AuthenticatedRequest } from "../../../../types/index.js"; @@ -54,7 +55,7 @@ export function managerHandler( error: error instanceof Error ? error.message : String(error), }); return res.status(500).json({ - error: error instanceof Error ? error.message : "Operation failed", + error: getErrorMessage(error, "Operation failed"), }); } }; diff --git a/src/backend/hosts/metrics/managers/validation.ts b/src/backend/hosts/metrics/managers/validation.ts index 73e10ad..7f42cb7 100644 --- a/src/backend/hosts/metrics/managers/validation.ts +++ b/src/backend/hosts/metrics/managers/validation.ts @@ -64,12 +64,7 @@ export function isValidSignal(sig: unknown): sig is Signal { } export type ServiceAction = - | "start" - | "stop" - | "restart" - | "reload" - | "enable" - | "disable"; + "start" | "stop" | "restart" | "reload" | "enable" | "disable"; const SERVICE_ACTIONS: ServiceAction[] = [ "start", "stop", diff --git a/src/backend/hosts/metrics/managers/wireguard.ts b/src/backend/hosts/metrics/managers/wireguard.ts index 018bc6c..8304c63 100644 --- a/src/backend/hosts/metrics/managers/wireguard.ts +++ b/src/backend/hosts/metrics/managers/wireguard.ts @@ -1,7 +1,6 @@ import type { Express } from "express"; import { execElevated } from "./exec-elevated.js"; -import { managerHandler } from "./route-helpers.js"; -import { ManagerInputError } from "./route-helpers.js"; +import { managerHandler, ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; import { isValidWireGuardInterface, diff --git a/src/backend/hosts/metrics/proxmox-stats-history-routes.ts b/src/backend/hosts/metrics/proxmox-stats-history-routes.ts new file mode 100644 index 0000000..63a095a --- /dev/null +++ b/src/backend/hosts/metrics/proxmox-stats-history-routes.ts @@ -0,0 +1,135 @@ +import type { Express, RequestHandler } from "express"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import { createCurrentProxmoxNodeHistoryRepository } from "../../database/repositories/factory.js"; +import { statsLogger } from "../../utils/logger.js"; +import type { HostAction } from "../../utils/permission-manager.js"; + +type ProxmoxStatsHistoryRoutesDeps = { + validateHostId: RequestHandler; + canAccessHost: ( + userId: string, + hostId: number, + level: HostAction, + ) => Promise; +}; + +const RANGE_OFFSETS: Record = { + "1h": 1 * 60 * 60 * 1000, + "6h": 6 * 60 * 60 * 1000, + "24h": 24 * 60 * 60 * 1000, + "7d": 7 * 24 * 60 * 60 * 1000, + "30d": 30 * 24 * 60 * 60 * 1000, +}; + +export function registerProxmoxStatsHistoryRoutes( + app: Express, + { validateHostId, canAccessHost }: ProxmoxStatsHistoryRoutesDeps, +): void { + /** + * @openapi + * /proxmox-stats/history/{hostId}: + * get: + * summary: Get historical Proxmox node stats for a host + * tags: + * - Proxmox Stats + * parameters: + * - in: path + * name: hostId + * required: true + * schema: + * type: integer + * - in: query + * name: range + * schema: + * type: string + * enum: [1h, 6h, 24h, 7d, 30d] + * - in: query + * name: from + * schema: + * type: string + * format: date-time + * - in: query + * name: to + * schema: + * type: string + * format: date-time + * responses: + * 200: + * description: Array of node history rows. + * 403: + * description: Access denied. + */ + app.get( + "/proxmox-stats/history/:hostId", + validateHostId, + async (req, res) => { + const hostId = Number(req.params.hostId); + const userId = (req as AuthenticatedRequest).userId; + + try { + const hasAccess = await canAccessHost(userId, hostId, "connect"); + if (!hasAccess) { + return res.status(403).json({ error: "Access denied" }); + } + + const { range, from, to } = req.query as Record< + string, + string | undefined + >; + + let fromTs: string; + let toTs: string = new Date().toISOString(); + + if (range) { + const offsetMs = RANGE_OFFSETS[range]; + if (!offsetMs) { + return res + .status(400) + .json({ error: "Invalid range. Use 1h, 6h, 24h, 7d, or 30d" }); + } + fromTs = new Date(Date.now() - offsetMs).toISOString(); + } else if (from && to) { + const fromDate = new Date(from); + const toDate = new Date(to); + if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { + return res + .status(400) + .json({ error: "Invalid from/to date format" }); + } + fromTs = fromDate.toISOString(); + toTs = toDate.toISOString(); + } else { + fromTs = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + } + + const toSqlite = (iso: string) => + iso.replace("T", " ").replace(/\.\d{3}Z$/, ""); + const rows = ( + await createCurrentProxmoxNodeHistoryRepository().listRange( + hostId, + toSqlite(fromTs), + toSqlite(toTs), + ) + ).map((row) => ({ + ts: row.ts, + cpu_percent: row.cpuPercent, + mem_percent: row.memPercent, + disk_percent: row.diskPercent, + net_rx_bytes: row.netRxBytes, + net_tx_bytes: row.netTxBytes, + })); + + res.json({ rows, fromTs, toTs }); + } catch (error) { + statsLogger.error("Failed to fetch proxmox stats history", { + operation: "proxmox_stats_history_fetch_error", + hostId, + error: error instanceof Error ? error.message : String(error), + }); + res + .status(500) + .json({ error: "Failed to fetch proxmox stats history" }); + } + }, + ); +} diff --git a/src/backend/hosts/metrics/proxmox-stats-polling.ts b/src/backend/hosts/metrics/proxmox-stats-polling.ts new file mode 100644 index 0000000..87c9d78 --- /dev/null +++ b/src/backend/hosts/metrics/proxmox-stats-polling.ts @@ -0,0 +1,332 @@ +import { statsLogger } from "../../utils/logger.js"; +import { + createCurrentProxmoxNodeHistoryRepository, + getCurrentSettingValue, +} from "../../database/repositories/factory.js"; +import { ConcurrentLimiter } from "./state.js"; +import { + collectProxmoxStats, + type ProxmoxStatsSnapshot, +} from "./proxmox/collect-proxmox-stats.js"; +import type { Client } from "ssh2"; + +/** + * Proxmox Stats polling gets its own limiter, separate from Host Metrics' + * metricsPollLimiter in state.ts, so a burst of Proxmox polls can never starve + * regular Host Metrics collection (or vice versa). + */ +const proxmoxStatsPollLimiter = new ConcurrentLimiter(5); + +export interface ProxmoxStatsPollableHost { + id: number; + userId: string; + proxmoxStatsConfig?: string | ProxmoxStatsPollConfig | null; +} + +export interface ProxmoxStatsPollConfig { + nodeName?: string | null; + pollInterval?: number; + enabledCards?: string[]; +} + +const DEFAULT_POLL_INTERVAL_SECONDS = 60; + +export function parseProxmoxStatsConfig( + raw: string | ProxmoxStatsPollConfig | null | undefined, +): ProxmoxStatsPollConfig { + if (!raw) { + return { nodeName: null, pollInterval: DEFAULT_POLL_INTERVAL_SECONDS }; + } + if (typeof raw === "object") { + return { + nodeName: raw.nodeName ?? null, + pollInterval: raw.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS, + enabledCards: raw.enabledCards, + }; + } + try { + const parsed = JSON.parse(raw) as ProxmoxStatsPollConfig; + return { + nodeName: parsed.nodeName ?? null, + pollInterval: parsed.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS, + enabledCards: parsed.enabledCards, + }; + } catch { + return { nodeName: null, pollInterval: DEFAULT_POLL_INTERVAL_SECONDS }; + } +} + +interface HostPollingEntry { + host: THost; + timer?: NodeJS.Timeout; + viewerUserId?: string; +} + +interface ViewerDetail { + sessionId: string; + userId: string; + hostId: number; + lastHeartbeat: number; +} + +interface CachedSnapshot { + data: ProxmoxStatsSnapshot; + timestamp: number; +} + +interface ErrorSnapshot { + error: string; + timestamp: number; +} + +export class ProxmoxPollingManager< + THost extends ProxmoxStatsPollableHost = ProxmoxStatsPollableHost, +> { + private pollingConfigs = new Map>(); + private snapshotStore = new Map(); + private errorStore = new Map(); + private activeViewers = new Map>(); + private viewerDetails = new Map(); + private inFlight = new Set(); + private viewerCleanupInterval: NodeJS.Timeout; + + constructor( + private readonly deps: { + fetchHostById: ( + hostId: number, + userId: string, + ) => Promise; + withSshConnection: ( + host: THost, + fn: (client: Client) => Promise, + ) => Promise; + historyEnabled?: () => boolean; + }, + ) { + this.viewerCleanupInterval = setInterval(() => { + this.cleanupInactiveViewers(); + }, 60000); + } + + private intervalWithJitter(intervalMs: number, hostId: number): number { + const spread = Math.min(intervalMs * 0.2, 15_000); + const jitter = (hostId * 1103515245) % Math.max(1, Math.floor(spread)); + return intervalMs + jitter; + } + + private getRetentionDays(): number { + try { + const value = getCurrentSettingValue("metrics_history_retention_days"); + const days = value ? parseInt(value, 10) : 7; + return isNaN(days) || days < 1 ? 7 : Math.min(days, 90); + } catch { + return 7; + } + } + + private async pollHostStats( + host: THost, + viewerUserId?: string, + ): Promise { + if (this.inFlight.has(host.id)) return; + this.inFlight.add(host.id); + + try { + await proxmoxStatsPollLimiter.run(async () => { + const userId = viewerUserId || host.userId; + const refreshed = + (await this.deps.fetchHostById(host.id, userId)) ?? host; + const config = parseProxmoxStatsConfig(refreshed.proxmoxStatsConfig); + + try { + const snapshot = await this.deps.withSshConnection( + refreshed, + (client) => collectProxmoxStats(client, config.nodeName), + ); + + this.snapshotStore.set(host.id, { + data: snapshot, + timestamp: Date.now(), + }); + this.errorStore.delete(host.id); + + if (this.deps.historyEnabled?.() ?? true) { + await this.insertHistory(host.id, snapshot); + } + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + this.errorStore.set(host.id, { + error: message, + timestamp: Date.now(), + }); + statsLogger.warn("Proxmox stats poll failed", { + operation: "proxmox_stats_poll_failed", + hostId: host.id, + error: message, + }); + } + }); + } finally { + this.inFlight.delete(host.id); + } + } + + private async insertHistory( + hostId: number, + snapshot: ProxmoxStatsSnapshot, + ): Promise { + try { + const iface = snapshot.network?.interfaces?.[0]; + const rxRaw = iface?.rxBytes ? parseInt(iface.rxBytes, 10) : null; + const txRaw = iface?.txBytes ? parseInt(iface.txBytes, 10) : null; + + const repository = createCurrentProxmoxNodeHistoryRepository(); + await repository.create({ + hostId, + cpuPercent: snapshot.node?.cpu?.percent ?? null, + memPercent: snapshot.node?.memory?.percent ?? null, + diskPercent: snapshot.node?.disk?.percent ?? null, + netRxBytes: rxRaw !== null && !isNaN(rxRaw) ? rxRaw : null, + netTxBytes: txRaw !== null && !isNaN(txRaw) ? txRaw : null, + }); + + await repository.pruneOlderThan(hostId, this.getRetentionDays()); + } catch (err) { + statsLogger.warn("Failed to write proxmox node history", { + operation: "insert_proxmox_node_history", + hostId, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + private startPollingForHost(host: THost, viewerUserId?: string): void { + const existing = this.pollingConfigs.get(host.id); + if (existing?.timer) { + clearInterval(existing.timer); + } + + const config = parseProxmoxStatsConfig(host.proxmoxStatsConfig); + const intervalMs = this.intervalWithJitter( + (config.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000, + host.id, + ); + + void this.pollHostStats(host, viewerUserId); + + const timer = setInterval(() => { + const latest = this.pollingConfigs.get(host.id); + if (latest) { + void this.pollHostStats(latest.host, latest.viewerUserId); + } + }, intervalMs); + + this.pollingConfigs.set(host.id, { host, timer, viewerUserId }); + } + + private stopPollingForHost(hostId: number): void { + const config = this.pollingConfigs.get(hostId); + if (config?.timer) { + clearInterval(config.timer); + } + this.pollingConfigs.delete(hostId); + } + + getStats( + hostId: number, + ): { data: ProxmoxStatsSnapshot; timestamp: number } | undefined { + return this.snapshotStore.get(hostId); + } + + getError(hostId: number): ErrorSnapshot | undefined { + return this.errorStore.get(hostId); + } + + async ensurePolling(host: THost, viewerUserId?: string): Promise { + if (!this.pollingConfigs.has(host.id)) { + this.startPollingForHost(host, viewerUserId); + } + if (!this.snapshotStore.has(host.id) && !this.inFlight.has(host.id)) { + await this.pollHostStats(host, viewerUserId); + } + } + + registerViewer = ( + hostId: number, + sessionId: string, + userId: string, + ): void => { + if (!this.activeViewers.has(hostId)) { + this.activeViewers.set(hostId, new Set()); + } + this.activeViewers.get(hostId)!.add(sessionId); + + this.viewerDetails.set(sessionId, { + sessionId, + userId, + hostId, + lastHeartbeat: Date.now(), + }); + + if (this.activeViewers.get(hostId)!.size === 1) { + Promise.resolve() + .then(async () => { + const host = await this.deps.fetchHostById(hostId, userId); + if (host) { + this.startPollingForHost(host, userId); + } + }) + .catch((err) => { + statsLogger.warn( + "Proxmox stats startPollingForHost rejected (non-fatal)", + { + operation: "proxmox_stats_start_unhandled", + hostId, + userId, + error: err instanceof Error ? err.message : String(err), + }, + ); + }); + } + }; + + unregisterViewer = (hostId: number, sessionId: string): void => { + const viewers = this.activeViewers.get(hostId); + if (viewers) { + viewers.delete(sessionId); + if (viewers.size === 0) { + this.activeViewers.delete(hostId); + this.stopPollingForHost(hostId); + } + } + this.viewerDetails.delete(sessionId); + }; + + updateHeartbeat(sessionId: string): boolean { + const viewer = this.viewerDetails.get(sessionId); + if (viewer) { + viewer.lastHeartbeat = Date.now(); + return true; + } + return false; + } + + private cleanupInactiveViewers(): void { + const now = Date.now(); + const maxInactivity = 120000; + + for (const [sessionId, viewer] of this.viewerDetails.entries()) { + if (now - viewer.lastHeartbeat > maxInactivity) { + this.unregisterViewer(viewer.hostId, sessionId); + } + } + } + + destroy(): void { + clearInterval(this.viewerCleanupInterval); + for (const hostId of this.pollingConfigs.keys()) { + this.stopPollingForHost(hostId); + } + } +} diff --git a/src/backend/hosts/metrics/proxmox-stats-routes.ts b/src/backend/hosts/metrics/proxmox-stats-routes.ts new file mode 100644 index 0000000..725c0e0 --- /dev/null +++ b/src/backend/hosts/metrics/proxmox-stats-routes.ts @@ -0,0 +1,258 @@ +import { getErrorMessage } from "../../utils/error-message.js"; +import type { Express, RequestHandler } from "express"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import { statsLogger } from "../../utils/logger.js"; +import { DataCrypto } from "../../utils/data-crypto.js"; +import type { HostAction } from "../../utils/permission-manager.js"; +import { + type ProxmoxPollingManager, + type ProxmoxStatsPollableHost, +} from "./proxmox-stats-polling.js"; + +const EMPTY_SNAPSHOT = { + node: { + cpu: { percent: null, cores: null, load: null }, + memory: { percent: null, usedGiB: null, totalGiB: null }, + disk: { percent: null, usedGiB: null, totalGiB: null }, + uptime: { seconds: null, formatted: null }, + system: { hostname: null, kernel: null, pveVersion: null }, + }, + network: { interfaces: [] }, + guests: { guests: [], counts: { running: 0, stopped: 0, total: 0 } }, + storage: { pools: [] }, + cluster: { clustered: false }, + lastChecked: new Date(0).toISOString(), +}; + +type ProxmoxStatsHost = ProxmoxStatsPollableHost & { + enableProxmoxStats?: boolean; +}; + +type ProxmoxStatsRoutesDeps = { + validateHostId: RequestHandler; + fetchHostById: ( + hostId: number, + userId: string, + ) => Promise; + canAccessHost: ( + userId: string, + hostId: number, + level: HostAction, + ) => Promise; + pollingManager: ProxmoxPollingManager; +}; + +export function registerProxmoxStatsRoutes( + app: Express, + { + validateHostId, + fetchHostById, + canAccessHost, + pollingManager, + }: ProxmoxStatsRoutesDeps, +): void { + /** + * @openapi + * /proxmox-stats/{id}: + * get: + * summary: Get cached Proxmox node stats for a host + * description: Returns the most recently polled Proxmox Stats snapshot for a host, or an empty skeleton if none has been collected yet. + * tags: + * - Proxmox Stats + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Proxmox stats snapshot. + * 401: + * description: Session expired - please log in again. + * 404: + * description: Stats not available yet. + */ + app.get("/proxmox-stats/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + const userId = (req as AuthenticatedRequest).userId; + + if (DataCrypto.getUserDataKey(userId) === null) { + return res.status(401).json({ + error: "Session expired - please log in again", + code: "SESSION_EXPIRED", + }); + } + + const cached = pollingManager.getStats(id); + if (!cached) { + const errorState = pollingManager.getError(id); + return res.status(404).json({ + error: errorState?.error || "Stats not available", + ...EMPTY_SNAPSHOT, + lastChecked: new Date().toISOString(), + }); + } + + res.json({ + ...cached.data, + lastChecked: new Date(cached.timestamp).toISOString(), + }); + }); + + /** + * @openapi + * /proxmox-stats/start/{id}: + * post: + * summary: Start Proxmox stats collection + * description: Registers a viewer and starts (or reuses) polling for a host's Proxmox node stats. + * tags: + * - Proxmox Stats + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Polling started, snapshot returned if already available. + * 401: + * description: Session expired - please log in again. + * 403: + * description: Proxmox Stats is not enabled for this host. + * 404: + * description: Host not found. + */ + app.post("/proxmox-stats/start/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + const userId = (req as AuthenticatedRequest).userId; + + if (DataCrypto.getUserDataKey(userId) === null) { + return res.status(401).json({ + error: "Session expired - please log in again", + code: "SESSION_EXPIRED", + }); + } + + try { + if (!(await canAccessHost(userId, id, "connect"))) { + return res.status(403).json({ error: "No access to this host" }); + } + + const host = await fetchHostById(id, userId); + if (!host) { + return res.status(404).json({ error: "Host not found" }); + } + + if (!host.enableProxmoxStats) { + return res + .status(403) + .json({ error: "Proxmox Stats is not enabled for this host" }); + } + + const viewerSessionId = `proxmox-viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + pollingManager.registerViewer(id, viewerSessionId, userId); + await pollingManager.ensurePolling(host, userId); + + const cached = pollingManager.getStats(id); + if (cached) { + return res.json({ + success: true, + viewerSessionId, + ...cached.data, + lastChecked: new Date(cached.timestamp).toISOString(), + }); + } + + const errorState = pollingManager.getError(id); + if (errorState) { + return res.json({ + success: true, + viewerSessionId, + status: "error", + error: errorState.error, + }); + } + + return res.json({ + success: true, + viewerSessionId, + status: "collecting", + }); + } catch (error) { + statsLogger.error("Failed to start proxmox stats collection", { + operation: "proxmox_stats_start_error", + hostId: id, + error: error instanceof Error ? error.message : String(error), + }); + res.status(500).json({ + error: getErrorMessage( + error, + "Failed to start proxmox stats collection", + ), + }); + } + }); + + /** + * @openapi + * /proxmox-stats/stop/{id}: + * post: + * summary: Stop Proxmox stats collection + * description: Unregisters a viewer session for a host's Proxmox node stats polling. + * tags: + * - Proxmox Stats + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: false + * content: + * application/json: + * schema: + * type: object + * properties: + * viewerSessionId: + * type: string + * responses: + * 200: + * description: Polling stopped successfully. + * 401: + * description: Session expired - please log in again. + */ + app.post("/proxmox-stats/stop/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + const userId = (req as AuthenticatedRequest).userId; + const { viewerSessionId } = req.body as { viewerSessionId?: string }; + + if (DataCrypto.getUserDataKey(userId) === null) { + return res.status(401).json({ + error: "Session expired - please log in again", + code: "SESSION_EXPIRED", + }); + } + + try { + if (viewerSessionId && typeof viewerSessionId === "string") { + pollingManager.unregisterViewer(id, viewerSessionId); + } + res.json({ success: true }); + } catch (error) { + statsLogger.error("Failed to stop proxmox stats collection", { + operation: "proxmox_stats_stop_error", + hostId: id, + error: error instanceof Error ? error.message : String(error), + }); + res.status(500).json({ + error: getErrorMessage( + error, + "Failed to stop proxmox stats collection", + ), + }); + } + }); +} diff --git a/src/backend/hosts/metrics/proxmox/cluster-health-collector.ts b/src/backend/hosts/metrics/proxmox/cluster-health-collector.ts new file mode 100644 index 0000000..6b04818 --- /dev/null +++ b/src/backend/hosts/metrics/proxmox/cluster-health-collector.ts @@ -0,0 +1,67 @@ +import type { Client } from "ssh2"; +import { execCommand } from "../widgets/common-utils.js"; + +export interface ProxmoxClusterNodeEntry { + name: string; + online: boolean; + local: boolean; + ip: string | null; +} + +export type ProxmoxClusterHealthResult = + | { clustered: false } + | { + clustered: true; + quorate: boolean; + clusterName: string | null; + nodes: ProxmoxClusterNodeEntry[]; + }; + +const EMPTY_RESULT: ProxmoxClusterHealthResult = { clustered: false }; + +export async function collectProxmoxClusterHealth( + client: Client, +): Promise { + try { + const { stdout, code } = await execCommand( + client, + "pvesh get /cluster/status --output-format json", + 15000, + ); + if (code !== 0) { + return EMPTY_RESULT; + } + + const data = JSON.parse(stdout); + if (!Array.isArray(data)) { + return EMPTY_RESULT; + } + + const entries = data as Array>; + const clusterEntry = entries.find((e) => e.type === "cluster"); + if (!clusterEntry) { + return EMPTY_RESULT; + } + + const nodes: ProxmoxClusterNodeEntry[] = entries + .filter((e) => e.type === "node") + .map((e) => ({ + name: typeof e.name === "string" ? e.name : "", + online: e.online === 1 || e.online === true, + local: e.local === 1 || e.local === true, + ip: typeof e.ip === "string" && e.ip ? e.ip : null, + })); + + return { + clustered: true, + quorate: clusterEntry.quorate === 1 || clusterEntry.quorate === true, + clusterName: + typeof clusterEntry.name === "string" && clusterEntry.name + ? clusterEntry.name + : null, + nodes, + }; + } catch { + return EMPTY_RESULT; + } +} diff --git a/src/backend/hosts/metrics/proxmox/collect-proxmox-stats.ts b/src/backend/hosts/metrics/proxmox/collect-proxmox-stats.ts new file mode 100644 index 0000000..7f822d1 --- /dev/null +++ b/src/backend/hosts/metrics/proxmox/collect-proxmox-stats.ts @@ -0,0 +1,86 @@ +import type { Client } from "ssh2"; +import { execCommand } from "../widgets/common-utils.js"; +import { isSafeNodeName } from "../../proxmox-shared.js"; +import { + collectProxmoxNodeStatus, + type ProxmoxNodeStatusResult, +} from "./node-status-collector.js"; +import { + collectProxmoxNodeNetwork, + type ProxmoxNodeNetworkResult, +} from "./node-network-collector.js"; +import { + collectProxmoxGuestsSummary, + type ProxmoxGuestsSummaryResult, +} from "./guests-collector.js"; +import { + collectProxmoxStorage, + type ProxmoxStorageResult, +} from "./storage-collector.js"; +import { + collectProxmoxClusterHealth, + type ProxmoxClusterHealthResult, +} from "./cluster-health-collector.js"; + +export interface ProxmoxStatsSnapshot { + node: ProxmoxNodeStatusResult; + network: ProxmoxNodeNetworkResult; + guests: ProxmoxGuestsSummaryResult; + storage: ProxmoxStorageResult; + cluster: ProxmoxClusterHealthResult; + lastChecked: string; +} + +async function resolveNodeName( + client: Client, + configuredNodeName: string | null | undefined, +): Promise { + if (configuredNodeName) { + // An explicitly configured name is validated and used as-is - an unsafe + // value is rejected outright rather than silently falling back to + // auto-detection, which would mask a misconfigured (or malicious) override. + if (!isSafeNodeName(configuredNodeName)) { + throw new Error("Unable to determine a valid Proxmox node name"); + } + return configuredNodeName; + } + + const { stdout } = await execCommand(client, "hostname", 10000); + return stdout.trim(); +} + +export async function collectProxmoxStats( + client: Client, + configuredNodeName: string | null | undefined, +): Promise { + const pveshCheck = await execCommand( + client, + "command -v pvesh >/dev/null 2>&1 && echo ok || echo missing", + 10000, + ); + if (pveshCheck.stdout.trim() !== "ok") { + throw new Error("pvesh not found โ€” is this a Proxmox node?"); + } + + const nodeName = await resolveNodeName(client, configuredNodeName); + if (!isSafeNodeName(nodeName)) { + throw new Error("Unable to determine a valid Proxmox node name"); + } + + const [node, network, guests, storage, cluster] = await Promise.all([ + collectProxmoxNodeStatus(client, nodeName), + collectProxmoxNodeNetwork(client, nodeName), + collectProxmoxGuestsSummary(client, nodeName), + collectProxmoxStorage(client, nodeName), + collectProxmoxClusterHealth(client), + ]); + + return { + node, + network, + guests, + storage, + cluster, + lastChecked: new Date().toISOString(), + }; +} diff --git a/src/backend/hosts/metrics/proxmox/guests-collector.ts b/src/backend/hosts/metrics/proxmox/guests-collector.ts new file mode 100644 index 0000000..704f36c --- /dev/null +++ b/src/backend/hosts/metrics/proxmox/guests-collector.ts @@ -0,0 +1,113 @@ +import type { Client } from "ssh2"; +import { execCommand, toFixedNum } from "../widgets/common-utils.js"; +import { isSafeNodeName } from "../../proxmox-shared.js"; + +export interface ProxmoxGuestSummaryEntry { + vmid: number; + name: string; + type: "qemu" | "lxc"; + status: string; + cpuPercent: number | null; + memPercent: number | null; + memUsedGiB: number | null; + memTotalGiB: number | null; + diskPercent: number | null; + diskUsedGiB: number | null; + diskTotalGiB: number | null; + uptimeSeconds: number | null; +} + +export interface ProxmoxGuestsSummaryResult { + guests: ProxmoxGuestSummaryEntry[]; + counts: { running: number; stopped: number; total: number }; +} + +const EMPTY_RESULT: ProxmoxGuestsSummaryResult = { + guests: [], + counts: { running: 0, stopped: 0, total: 0 }, +}; + +function bytesToGiB(bytes: number): number { + return bytes / (1024 * 1024 * 1024); +} + +export async function collectProxmoxGuestsSummary( + client: Client, + nodeName: string, +): Promise { + if (!isSafeNodeName(nodeName)) { + return EMPTY_RESULT; + } + + try { + const { stdout, code } = await execCommand( + client, + "pvesh get /cluster/resources --output-format json", + 25000, + ); + if (code !== 0) { + return EMPTY_RESULT; + } + + const resources = JSON.parse(stdout); + if (!Array.isArray(resources)) { + return EMPTY_RESULT; + } + + const guests: ProxmoxGuestSummaryEntry[] = []; + for (const r of resources as Array>) { + const type = r.type; + if (type !== "qemu" && type !== "lxc") continue; + if (r.node !== nodeName) continue; + if (r.template === true || r.template === 1) continue; + + const cpuFraction = typeof r.cpu === "number" ? r.cpu : null; + const mem = typeof r.mem === "number" ? r.mem : null; + const maxmem = typeof r.maxmem === "number" ? r.maxmem : null; + const memPercent = + mem !== null && maxmem !== null && maxmem > 0 + ? Math.max(0, Math.min(100, (mem / maxmem) * 100)) + : null; + + const disk = typeof r.disk === "number" ? r.disk : null; + const maxdisk = typeof r.maxdisk === "number" ? r.maxdisk : null; + // QEMU guests without a running agent report maxdisk: 0 - a false 0% + // is worse than an honest "unknown", so treat that as no data at all. + const hasDiskData = maxdisk !== null && maxdisk > 0; + const diskPercent = + hasDiskData && disk !== null + ? Math.max(0, Math.min(100, (disk / maxdisk) * 100)) + : null; + + guests.push({ + vmid: typeof r.vmid === "number" ? r.vmid : Number(r.vmid), + name: + typeof r.name === "string" && r.name ? r.name : String(r.vmid ?? ""), + type, + status: typeof r.status === "string" ? r.status : "unknown", + cpuPercent: toFixedNum( + cpuFraction !== null ? cpuFraction * 100 : null, + 0, + ), + memPercent: toFixedNum(memPercent, 0), + memUsedGiB: mem !== null ? toFixedNum(bytesToGiB(mem), 2) : null, + memTotalGiB: maxmem !== null ? toFixedNum(bytesToGiB(maxmem), 2) : null, + diskPercent: toFixedNum(diskPercent, 0), + diskUsedGiB: + hasDiskData && disk !== null ? toFixedNum(bytesToGiB(disk), 2) : null, + diskTotalGiB: hasDiskData ? toFixedNum(bytesToGiB(maxdisk), 2) : null, + uptimeSeconds: typeof r.uptime === "number" ? r.uptime : null, + }); + } + + const running = guests.filter((g) => g.status === "running").length; + const stopped = guests.filter((g) => g.status !== "running").length; + + return { + guests, + counts: { running, stopped, total: guests.length }, + }; + } catch { + return EMPTY_RESULT; + } +} diff --git a/src/backend/hosts/metrics/proxmox/node-network-collector.ts b/src/backend/hosts/metrics/proxmox/node-network-collector.ts new file mode 100644 index 0000000..faa9502 --- /dev/null +++ b/src/backend/hosts/metrics/proxmox/node-network-collector.ts @@ -0,0 +1,145 @@ +import type { Client } from "ssh2"; +import { execCommand } from "../widgets/common-utils.js"; +import { isSafeNodeName } from "../../proxmox-shared.js"; + +export interface ProxmoxNodeNetworkInterface { + name: string; + ip: string | null; + state: string | null; + rxBytes: string | null; + txBytes: string | null; +} + +export interface ProxmoxNodeNetworkResult { + interfaces: ProxmoxNodeNetworkInterface[]; +} + +const EMPTY_RESULT: ProxmoxNodeNetworkResult = { interfaces: [] }; + +async function collectFromProcNetDev( + client: Client, +): Promise { + const interfaces: ProxmoxNodeNetworkInterface[] = []; + + try { + const [addrOut, stateOut, procNetOut] = await Promise.all([ + execCommand( + client, + "ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'", + ), + execCommand( + client, + "ip -o link show | awk '{gsub(/:/, \"\", $2); print $2,$9}'", + ), + execCommand(client, "cat /proc/net/dev"), + ]); + + const ifMap = new Map< + string, + { ip: string | null; state: string | null } + >(); + for (const line of addrOut.stdout + .split("\n") + .map((l) => l.trim()) + .filter(Boolean)) { + const parts = line.split(/\s+/); + if (parts.length >= 2) { + const name = parts[0]; + const ip = parts[1].split("/")[0]; + if (!ifMap.has(name)) ifMap.set(name, { ip, state: null }); + } + } + for (const line of stateOut.stdout + .split("\n") + .map((l) => l.trim()) + .filter(Boolean)) { + const parts = line.split(/\s+/); + if (parts.length >= 2) { + const existing = ifMap.get(parts[0]); + if (existing) existing.state = parts[1]; + } + } + + const rxTxMap = new Map(); + for (const line of procNetOut.stdout.split("\n").slice(2)) { + const parts = line.trim().split(/\s+/); + if (parts.length >= 10) { + const ifName = parts[0].replace(":", ""); + rxTxMap.set(ifName, { rx: parts[1], tx: parts[9] }); + } + } + + for (const [name, data] of ifMap.entries()) { + const rxTx = rxTxMap.get(name); + interfaces.push({ + name, + ip: data.ip, + state: data.state, + rxBytes: rxTx?.rx ?? null, + txBytes: rxTx?.tx ?? null, + }); + } + } catch { + return EMPTY_RESULT; + } + + return { interfaces }; +} + +export async function collectProxmoxNodeNetwork( + client: Client, + nodeName: string, +): Promise { + if (!isSafeNodeName(nodeName)) { + return EMPTY_RESULT; + } + + try { + const { stdout, code } = await execCommand( + client, + `pvesh get /nodes/${nodeName}/netstat --output-format json`, + 15000, + ); + if (code !== 0) { + return collectFromProcNetDev(client); + } + + const data = JSON.parse(stdout); + if (!Array.isArray(data)) { + return collectFromProcNetDev(client); + } + + const interfaces: ProxmoxNodeNetworkInterface[] = data + .filter( + (entry): entry is Record => + !!entry && typeof entry === "object", + ) + .map((entry) => ({ + name: + typeof entry.dev === "string" ? entry.dev : String(entry.dev ?? ""), + ip: null, + state: null, + rxBytes: + typeof entry.in === "number" + ? String(entry.in) + : typeof entry.received === "number" + ? String(entry.received) + : null, + txBytes: + typeof entry.out === "number" + ? String(entry.out) + : typeof entry.transmitted === "number" + ? String(entry.transmitted) + : null, + })) + .filter((iface) => iface.name && iface.name !== "lo"); + + if (interfaces.length === 0) { + return collectFromProcNetDev(client); + } + + return { interfaces }; + } catch { + return collectFromProcNetDev(client); + } +} diff --git a/src/backend/hosts/metrics/proxmox/node-status-collector.ts b/src/backend/hosts/metrics/proxmox/node-status-collector.ts new file mode 100644 index 0000000..4b6fb00 --- /dev/null +++ b/src/backend/hosts/metrics/proxmox/node-status-collector.ts @@ -0,0 +1,142 @@ +import type { Client } from "ssh2"; +import { execCommand, toFixedNum } from "../widgets/common-utils.js"; +import { isSafeNodeName } from "../../proxmox-shared.js"; + +export interface ProxmoxNodeStatusResult { + cpu: { + percent: number | null; + cores: number | null; + load: [number, number, number] | null; + }; + memory: { + percent: number | null; + usedGiB: number | null; + totalGiB: number | null; + }; + disk: { + percent: number | null; + usedGiB: number | null; + totalGiB: number | null; + }; + uptime: { + seconds: number | null; + formatted: string | null; + }; + system: { + hostname: string | null; + kernel: string | null; + pveVersion: string | null; + }; +} + +const EMPTY_RESULT: ProxmoxNodeStatusResult = { + cpu: { percent: null, cores: null, load: null }, + memory: { percent: null, usedGiB: null, totalGiB: null }, + disk: { percent: null, usedGiB: null, totalGiB: null }, + uptime: { seconds: null, formatted: null }, + system: { hostname: null, kernel: null, pveVersion: null }, +}; + +function bytesToGiB(bytes: number): number { + return bytes / (1024 * 1024 * 1024); +} + +function formatUptime(seconds: number): string { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return `${days}d ${hours}h ${minutes}m`; +} + +export async function collectProxmoxNodeStatus( + client: Client, + nodeName: string, +): Promise { + if (!isSafeNodeName(nodeName)) { + return EMPTY_RESULT; + } + + try { + const { stdout, code } = await execCommand( + client, + `pvesh get /nodes/${nodeName}/status --output-format json`, + 25000, + ); + if (code !== 0) { + return EMPTY_RESULT; + } + + const data = JSON.parse(stdout) as Record; + + const cpuFraction = typeof data.cpu === "number" ? data.cpu : null; + const cpuinfo = (data.cpuinfo as Record) || {}; + const cores = typeof cpuinfo.cores === "number" ? cpuinfo.cores : null; + const loadavgRaw = data.loadavg; + let load: [number, number, number] | null = null; + if (Array.isArray(loadavgRaw) && loadavgRaw.length >= 3) { + const parsed = loadavgRaw + .slice(0, 3) + .map((v) => Number(v)) + .map((v) => (Number.isFinite(v) ? v : 0)); + load = parsed as [number, number, number]; + } + + const memory = (data.memory as Record) || {}; + const memUsed = typeof memory.used === "number" ? memory.used : null; + const memTotal = typeof memory.total === "number" ? memory.total : null; + const memPercent = + memUsed !== null && memTotal !== null && memTotal > 0 + ? Math.max(0, Math.min(100, (memUsed / memTotal) * 100)) + : null; + + const rootfs = (data.rootfs as Record) || {}; + const diskUsed = typeof rootfs.used === "number" ? rootfs.used : null; + const diskTotal = typeof rootfs.total === "number" ? rootfs.total : null; + const diskPercent = + diskUsed !== null && diskTotal !== null && diskTotal > 0 + ? Math.max(0, Math.min(100, (diskUsed / diskTotal) * 100)) + : null; + + const uptimeSeconds = typeof data.uptime === "number" ? data.uptime : null; + + return { + cpu: { + percent: toFixedNum(cpuFraction !== null ? cpuFraction * 100 : null, 0), + cores, + load, + }, + memory: { + percent: toFixedNum(memPercent, 0), + usedGiB: memUsed !== null ? toFixedNum(bytesToGiB(memUsed), 2) : null, + totalGiB: + memTotal !== null ? toFixedNum(bytesToGiB(memTotal), 2) : null, + }, + disk: { + percent: toFixedNum(diskPercent, 0), + usedGiB: diskUsed !== null ? toFixedNum(bytesToGiB(diskUsed), 2) : null, + totalGiB: + diskTotal !== null ? toFixedNum(bytesToGiB(diskTotal), 2) : null, + }, + uptime: { + seconds: uptimeSeconds, + formatted: uptimeSeconds !== null ? formatUptime(uptimeSeconds) : null, + }, + system: { + hostname: + typeof data.hostname === "string" && data.hostname + ? data.hostname + : null, + kernel: + typeof data.kversion === "string" && data.kversion + ? data.kversion + : null, + pveVersion: + typeof data.pveversion === "string" && data.pveversion + ? data.pveversion + : null, + }, + }; + } catch { + return EMPTY_RESULT; + } +} diff --git a/src/backend/hosts/metrics/proxmox/storage-collector.ts b/src/backend/hosts/metrics/proxmox/storage-collector.ts new file mode 100644 index 0000000..ff30624 --- /dev/null +++ b/src/backend/hosts/metrics/proxmox/storage-collector.ts @@ -0,0 +1,76 @@ +import type { Client } from "ssh2"; +import { execCommand, toFixedNum } from "../widgets/common-utils.js"; +import { isSafeNodeName } from "../../proxmox-shared.js"; + +export interface ProxmoxStoragePoolEntry { + name: string; + type: string; + active: boolean; + enabled: boolean; + usedGiB: number | null; + totalGiB: number | null; + availGiB: number | null; + percent: number | null; +} + +export interface ProxmoxStorageResult { + pools: ProxmoxStoragePoolEntry[]; +} + +const EMPTY_RESULT: ProxmoxStorageResult = { pools: [] }; + +function bytesToGiB(bytes: number): number { + return bytes / (1024 * 1024 * 1024); +} + +export async function collectProxmoxStorage( + client: Client, + nodeName: string, +): Promise { + if (!isSafeNodeName(nodeName)) { + return EMPTY_RESULT; + } + + try { + const { stdout, code } = await execCommand( + client, + `pvesh get /nodes/${nodeName}/storage --output-format json`, + 25000, + ); + if (code !== 0) { + return EMPTY_RESULT; + } + + const data = JSON.parse(stdout); + if (!Array.isArray(data)) { + return EMPTY_RESULT; + } + + const pools: ProxmoxStoragePoolEntry[] = ( + data as Array> + ).map((entry) => { + const used = typeof entry.used === "number" ? entry.used : null; + const total = typeof entry.total === "number" ? entry.total : null; + const avail = typeof entry.avail === "number" ? entry.avail : null; + const percent = + used !== null && total !== null && total > 0 + ? Math.max(0, Math.min(100, (used / total) * 100)) + : null; + + return { + name: typeof entry.storage === "string" ? entry.storage : "", + type: typeof entry.type === "string" ? entry.type : "unknown", + active: entry.active === 1 || entry.active === true, + enabled: entry.enabled === 1 || entry.enabled === true, + usedGiB: used !== null ? toFixedNum(bytesToGiB(used), 2) : null, + totalGiB: total !== null ? toFixedNum(bytesToGiB(total), 2) : null, + availGiB: avail !== null ? toFixedNum(bytesToGiB(avail), 2) : null, + percent: toFixedNum(percent, 0), + }; + }); + + return { pools }; + } catch { + return EMPTY_RESULT; + } +} diff --git a/src/backend/hosts/metrics/state.ts b/src/backend/hosts/metrics/state.ts index 7a2e4eb..3ccd8cf 100644 --- a/src/backend/hosts/metrics/state.ts +++ b/src/backend/hosts/metrics/state.ts @@ -258,7 +258,7 @@ export class ConcurrentLimiter { private active = 0; private readonly waiters: Array<() => void> = []; - constructor(private readonly maxConcurrent: number) { + constructor(private maxConcurrent: number) { if (maxConcurrent < 1) { throw new Error("maxConcurrent must be >= 1"); } @@ -272,11 +272,55 @@ export class ConcurrentLimiter { return this.waiters.length; } + get limit(): number { + return this.maxConcurrent; + } + + /** + * Changes the ceiling at runtime. + * + * Raising it wakes the waiters the new headroom allows, so a queue that built + * up under the old limit drains at once instead of one job at a time as + * running work finishes. Lowering it never interrupts work already running; + * the new limit simply applies from the next release onward. + */ + setLimit(maxConcurrent: number): void { + if (maxConcurrent < 1) { + throw new Error("maxConcurrent must be >= 1"); + } + this.maxConcurrent = maxConcurrent; + this.releaseWaiters(); + } + + /** + * Wakes as many queued callers as there is now room for. + * + * `woken` counts callers that have been resumed but have not yet reached the + * `active += 1` on the far side of their await. Without it the occupancy + * looks lower than it really is for a microtask, and the loop would release + * past the ceiling. + */ + private releaseWaiters(): void { + while ( + this.active + this.woken < this.maxConcurrent && + this.waiters.length > 0 + ) { + this.woken += 1; + this.waiters.shift()!(); + } + } + + private woken = 0; + async run(fn: () => Promise): Promise { - if (this.active >= this.maxConcurrent) { + // Queue behind anything already woken, so a caller arriving mid-handoff + // cannot jump the queue and push occupancy past the ceiling. + if (this.active + this.woken >= this.maxConcurrent) { await new Promise((resolve) => { this.waiters.push(resolve); }); + // Resumed by a slot handoff; that reservation is now consumed. + this.woken = Math.max(0, this.woken - 1); } this.active += 1; @@ -284,12 +328,42 @@ export class ConcurrentLimiter { return await fn(); } finally { this.active -= 1; - const next = this.waiters.shift(); - if (next) next(); + this.releaseWaiters(); } } } +/** + * How many metrics polls may run at once for a given number of polled hosts. + * + * A metrics poll is an SSH exec, so this cannot simply be unbounded โ€” but the + * old fixed ceiling of 5 meant a sweep of 500 hosts took ~40s against a 30s + * interval, so polling fell permanently behind and a host could wait over a + * minute for a "30 second" metric. Scaling with the fleet keeps a sweep inside + * its interval; the cap keeps file descriptors and CPU bounded. + */ +export const METRICS_CONCURRENCY_ENV = "METRICS_POLL_CONCURRENCY"; +const MIN_METRICS_CONCURRENCY = 5; +const MAX_METRICS_CONCURRENCY = 50; +/** Aim to spend about a twentieth of the interval per sweep wave. */ +const HOSTS_PER_WORKER = 20; + +export function metricsConcurrencyFor( + hostCount: number, + env: NodeJS.ProcessEnv = process.env, +): number { + const override = Number(env[METRICS_CONCURRENCY_ENV]); + if (Number.isFinite(override) && override >= 1) { + return Math.min(Math.floor(override), MAX_METRICS_CONCURRENCY); + } + + const scaled = Math.ceil(Math.max(0, hostCount) / HOSTS_PER_WORKER); + return Math.min( + Math.max(scaled, MIN_METRICS_CONCURRENCY), + MAX_METRICS_CONCURRENCY, + ); +} + /** Short-lived host snapshots for polling โ€” avoids decrypting host rows every tick. */ export class HostPollCache { private cache = new Map< @@ -330,9 +404,23 @@ export class HostPollCache { export const statusPollLimiter = new ConcurrentLimiter(20); /** SSH metrics execs are expensive; keep concurrency tight. */ export const metricsPollLimiter = new ConcurrentLimiter(5); +/** Viewer registration is bursty; admit only two first samples at a time. */ +export const initialMetricsPollLimiter = new ConcurrentLimiter(2); + +export function canStartInitialMetrics( + status: HostStatus | undefined, + hasViewers: boolean, + statusCheckEnabled = true, +): boolean { + return ( + hasViewers && + (!statusCheckEnabled || status === "reachable" || status === "online") + ); +} export const hostPollCache = new HostPollCache(30_000); export const requestQueue = new RequestQueue(); export const metricsCache = new MetricsCache(); export const authFailureTracker = new AuthFailureTracker(); export const pollingBackoff = new PollingBackoff(); +import type { HostStatus } from "./host-status.js"; diff --git a/src/backend/hosts/metrics/viewer-routes.ts b/src/backend/hosts/metrics/viewer-routes.ts index 0467585..b6086ad 100644 --- a/src/backend/hosts/metrics/viewer-routes.ts +++ b/src/backend/hosts/metrics/viewer-routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import type { Express } from "express"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { statsLogger } from "../../utils/logger.js"; @@ -21,6 +22,8 @@ type HostMetricsViewerRoutesDeps< userId: string, ) => void; unregisterViewer: (hostId: number, viewerSessionId: string) => void; + /** Route path segment, e.g. "metrics" -> /metrics/heartbeat. Defaults to "metrics". */ + pathPrefix?: string; }; export function registerHostMetricsViewerRoutes< @@ -35,6 +38,7 @@ export function registerHostMetricsViewerRoutes< updateHeartbeat, registerViewer, unregisterViewer, + pathPrefix = "metrics", }: HostMetricsViewerRoutesDeps, ): void { /** @@ -66,7 +70,7 @@ export function registerHostMetricsViewerRoutes< * 500: * description: Failed to update heartbeat. */ - app.post("/metrics/heartbeat", async (req, res) => { + app.post(`/${pathPrefix}/heartbeat`, async (req, res) => { const { viewerSessionId } = req.body; const userId = (req as AuthenticatedRequest).userId; @@ -125,7 +129,7 @@ export function registerHostMetricsViewerRoutes< * 500: * description: Failed to register viewer. */ - app.post("/metrics/register-viewer", async (req, res) => { + app.post(`/${pathPrefix}/register-viewer`, async (req, res) => { const { hostId } = req.body; const userId = (req as AuthenticatedRequest).userId; @@ -154,10 +158,7 @@ export function registerHostMetricsViewerRoutes< operation: "register_viewer_lookup", hostId, userId, - error: - lookupErr instanceof Error - ? lookupErr.message - : String(lookupErr), + error: getErrorMessage(lookupErr, String(lookupErr)), }, ); } @@ -255,7 +256,7 @@ export function registerHostMetricsViewerRoutes< * 500: * description: Failed to unregister viewer. */ - app.post("/metrics/unregister-viewer", async (req, res) => { + app.post(`/${pathPrefix}/unregister-viewer`, async (req, res) => { const { hostId, viewerSessionId } = req.body; const userId = (req as AuthenticatedRequest).userId; diff --git a/src/backend/hosts/metrics/widgets/disk-collector.ts b/src/backend/hosts/metrics/widgets/disk-collector.ts index 79ae76c..abeb405 100644 --- a/src/backend/hosts/metrics/widgets/disk-collector.ts +++ b/src/backend/hosts/metrics/widgets/disk-collector.ts @@ -1,67 +1,276 @@ import type { Client } from "ssh2"; import { execCommand, toFixedNum } from "./common-utils.js"; -export async function collectDiskMetrics(client: Client): Promise<{ +const PSEUDO_FS_RE = /^(tmpfs|devtmpfs|overlay|udev|none|shm)$/; + +export interface DfRow { + filesystem: string; + type: string; + mount: string; + parts: string[]; +} + +export interface DiskFilesystem { + filesystem: string; + type: string; + mount: string; percent: number | null; usedHuman: string | null; totalHuman: string | null; availableHuman: string | null; -}> { - let diskPercent: number | null = null; - let usedHuman: string | null = null; - let totalHuman: string | null = null; - let availableHuman: string | null = null; + usedBytes: number | null; + totalBytes: number | null; + availableBytes: number | null; + label?: string; +} - try { - const [diskOutHuman, diskOutBytes] = await Promise.all([ - execCommand(client, "df -h -P / | tail -n +2"), - execCommand(client, "df -B1 -P / | tail -n +2"), - ]); +export interface MonitoredMount { + path: string; + label?: string; +} - const humanLine = - diskOutHuman.stdout - .split("\n") - .map((l) => l.trim()) - .filter(Boolean)[0] || ""; - const bytesLine = - diskOutBytes.stdout - .split("\n") - .map((l) => l.trim()) - .filter(Boolean)[0] || ""; +function shellQuote(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; +} - const humanParts = humanLine.split(/\s+/); - const bytesParts = bytesLine.split(/\s+/); +// Parses `df -T -P`-style output: Filesystem, Type, then the size columns, +// with Mounted-on last. The Type column (e.g. ext4, nfs4, cifs) is what lets +// callers filter network shares out by filesystem type rather than guessing +// from the source path. +export function parseDfLines(output: string): DfRow[] { + return output + .split("\n") + .map((l) => l.trim()) + .filter(Boolean) + .map((line) => { + const parts = line.split(/\s+/); + return { + filesystem: parts[0] || "", + type: parts[1] || "", + mount: parts[6] || "", + parts, + }; + }) + .filter((row) => row.parts.length >= 7 && !PSEUDO_FS_RE.test(row.type)); +} - if (humanParts.length >= 6 && bytesParts.length >= 6) { - totalHuman = humanParts[1] || null; - usedHuman = humanParts[2] || null; - availableHuman = humanParts[3] || null; +// Finds the index of the most-utilized real filesystem in a `df -T -B1`-style +// row set (parts[2] = total bytes, parts[3] = used bytes), so a nearly-full +// secondary mount (e.g. /data) isn't hidden behind a healthy root filesystem. +export function findWorstMountIndex(bytesRows: DfRow[]): { + index: number; + usedBytes: number; + totalBytes: number; +} { + let worstIndex = -1; + let worstUsedBytes = -1; + let worstTotalBytes = 0; - const totalBytes = Number(bytesParts[1]); - const usedBytes = Number(bytesParts[2]); - - if ( - Number.isFinite(totalBytes) && - Number.isFinite(usedBytes) && - totalBytes > 0 - ) { - diskPercent = Math.max( - 0, - Math.min(100, (usedBytes / totalBytes) * 100), - ); - } + bytesRows.forEach((row, index) => { + const totalBytes = Number(row.parts[2]); + const usedBytes = Number(row.parts[3]); + if ( + !Number.isFinite(totalBytes) || + !Number.isFinite(usedBytes) || + totalBytes <= 0 + ) { + return; } - } catch { - diskPercent = null; - usedHuman = null; - totalHuman = null; - availableHuman = null; - } + const usedRatio = usedBytes / totalBytes; + const worstRatio = + worstTotalBytes > 0 ? worstUsedBytes / worstTotalBytes : -1; + if (usedRatio > worstRatio) { + worstIndex = index; + worstUsedBytes = usedBytes; + worstTotalBytes = totalBytes; + } + }); return { - percent: toFixedNum(diskPercent, 0), - usedHuman, - totalHuman, - availableHuman, + index: worstIndex, + usedBytes: worstUsedBytes, + totalBytes: worstTotalBytes, }; } + +// Merges the `df -T -B1` and `df -T -h` row sets into one filesystem list. +// Byte rows drive the maths; human rows only supply the display strings, +// matched by mount point so a mismatched row count can't shift the columns. +export function buildFilesystemList( + bytesRows: DfRow[], + humanRows: DfRow[], +): DiskFilesystem[] { + const aligned = humanRows.length === bytesRows.length; + + return bytesRows + .map((row, index) => { + const totalBytes = Number(row.parts[2]); + const usedBytes = Number(row.parts[3]); + const availableBytes = Number(row.parts[4]); + if (!Number.isFinite(totalBytes) || totalBytes <= 0) return null; + + const humanRow = aligned + ? humanRows[index] + : humanRows.find((h) => h.mount === row.mount); + + const percent = Number.isFinite(usedBytes) + ? Math.max(0, Math.min(100, (usedBytes / totalBytes) * 100)) + : null; + + return { + filesystem: row.filesystem, + type: row.type, + mount: row.mount, + percent: toFixedNum(percent, 0), + usedHuman: humanRow?.parts[3] || null, + totalHuman: humanRow?.parts[2] || null, + availableHuman: humanRow?.parts[4] || null, + usedBytes: Number.isFinite(usedBytes) ? usedBytes : null, + totalBytes, + availableBytes: Number.isFinite(availableBytes) ? availableBytes : null, + }; + }) + .filter((fs): fs is DiskFilesystem => fs !== null); +} + +// The headline disk figure should be the root filesystem - that is what users +// mean by "the server's disk". Only when there is no root mount (containers, +// chroots) do we fall back to the most-utilized mount. +export function selectPrimaryFilesystem( + filesystems: DiskFilesystem[], +): DiskFilesystem | null { + if (filesystems.length === 0) return null; + + const root = filesystems.find((fs) => fs.mount === "/"); + if (root) return root; + + let best = filesystems[0]; + for (const fs of filesystems) { + const ratio = (fs.usedBytes ?? 0) / (fs.totalBytes || 1); + const bestRatio = (best.usedBytes ?? 0) / (best.totalBytes || 1); + if (ratio > bestRatio) best = fs; + } + return best; +} + +// Excluded mounts are user-configured per host: an exact mount-path match +// (e.g. "/mnt/nas") or a filesystem-type substring match (e.g. "nfs" matches +// nfs/nfs4, "cifs" matches cifs/smb3), so network shares can be dropped from +// the headline percent and the filesystem list without hiding local disks. +export function filterExcludedFilesystems( + filesystems: DiskFilesystem[], + excludedMounts?: string[] | null, +): DiskFilesystem[] { + if (!excludedMounts || excludedMounts.length === 0) return filesystems; + + const normalized = excludedMounts + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean); + if (normalized.length === 0) return filesystems; + + return filesystems.filter((fs) => { + const mount = fs.mount.toLowerCase(); + const type = fs.type.toLowerCase(); + return !normalized.some( + (entry) => mount === entry || (type && type.includes(entry)), + ); + }); +} + +export function mergeMonitoredFilesystems( + filesystems: DiskFilesystem[], + monitored: MonitoredMount[], + customFilesystems: Array, +): DiskFilesystem[] { + const result = [...filesystems]; + monitored.forEach((entry, index) => { + const path = entry.path.trim(); + const custom = customFilesystems[index]; + if (!path || !custom) return; + + const existing = result.find((fs) => fs.mount === path); + if (existing) { + existing.label = entry.label?.trim() || undefined; + return; + } + result.push({ + ...custom, + mount: path, + label: entry.label?.trim() || undefined, + }); + }); + return result; +} + +export async function collectDiskMetrics( + client: Client, + excludedMounts?: string[] | null, + monitoredMounts?: MonitoredMount[] | null, +): Promise<{ + percent: number | null; + usedHuman: string | null; + totalHuman: string | null; + availableHuman: string | null; + mount: string | null; + filesystems: DiskFilesystem[]; +}> { + try { + const [diskOutHuman, diskOutBytes] = await Promise.all([ + execCommand(client, "df -hT -P | tail -n +2"), + execCommand(client, "df -TB1 -P | tail -n +2"), + ]); + + const humanRows = parseDfLines(diskOutHuman.stdout); + const bytesRows = parseDfLines(diskOutBytes.stdout); + let detected = buildFilesystemList(bytesRows, humanRows); + const monitored = (monitoredMounts ?? []).filter((entry) => + Boolean(entry.path.trim()), + ); + if (monitored.length > 0) { + const customFilesystems = await Promise.all( + monitored.map(async (entry) => { + const path = shellQuote(entry.path.trim()); + try { + const [customHuman, customBytes] = await Promise.all([ + execCommand(client, `df -hT -P -- ${path} | tail -n +2`), + execCommand(client, `df -TB1 -P -- ${path} | tail -n +2`), + ]); + return ( + buildFilesystemList( + parseDfLines(customBytes.stdout), + parseDfLines(customHuman.stdout), + )[0] ?? null + ); + } catch { + return null; + } + }), + ); + detected = mergeMonitoredFilesystems( + detected, + monitored, + customFilesystems, + ); + } + const filesystems = filterExcludedFilesystems(detected, excludedMounts); + const primary = selectPrimaryFilesystem(filesystems); + + return { + percent: primary?.percent ?? null, + usedHuman: primary?.usedHuman ?? null, + totalHuman: primary?.totalHuman ?? null, + availableHuman: primary?.availableHuman ?? null, + mount: primary?.mount ?? null, + filesystems, + }; + } catch { + return { + percent: null, + usedHuman: null, + totalHuman: null, + availableHuman: null, + mount: null, + filesystems: [], + }; + } +} diff --git a/src/backend/hosts/metrics/widgets/network-collector.ts b/src/backend/hosts/metrics/widgets/network-collector.ts index eec4248..f9e43c2 100644 --- a/src/backend/hosts/metrics/widgets/network-collector.ts +++ b/src/backend/hosts/metrics/widgets/network-collector.ts @@ -1,6 +1,45 @@ import type { Client } from "ssh2"; import { execCommand } from "./common-utils.js"; +export interface NetworkCounters { + rx: string; + tx: string; +} + +export function parseNetworkCounters( + output: string, +): Map { + const counters = new Map(); + for (const line of output.split("\n").slice(2)) { + const parts = line.trim().split(/\s+/); + if (parts.length >= 10) { + counters.set(parts[0].replace(":", ""), { + rx: parts[1], + tx: parts[9], + }); + } + } + return counters; +} + +export function counterRate( + before: string | undefined, + after: string | undefined, + elapsedSeconds: number, +): number | null { + const first = Number(before); + const second = Number(after); + if ( + !Number.isFinite(first) || + !Number.isFinite(second) || + second < first || + elapsedSeconds <= 0 + ) { + return null; + } + return Math.round((second - first) / elapsedSeconds); +} + export async function collectNetworkMetrics(client: Client): Promise<{ interfaces: Array<{ name: string; @@ -8,6 +47,8 @@ export async function collectNetworkMetrics(client: Client): Promise<{ state: string; rxBytes: string | null; txBytes: string | null; + rxRateBps: number | null; + txRateBps: number | null; }>; }> { const interfaces: Array<{ @@ -16,16 +57,18 @@ export async function collectNetworkMetrics(client: Client): Promise<{ state: string; rxBytes: string | null; txBytes: string | null; + rxRateBps: number | null; + txRateBps: number | null; }> = []; try { const ifconfigOut = await execCommand( client, - "ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'", + "ip -o addr show 2>/dev/null | awk '{print $2,$4}' | grep -v '^lo' || true", ); const netStatOut = await execCommand( client, - "ip -o link show | awk '{gsub(/:/, \"\", $2); print $2,$9}'", + "ip -o link show 2>/dev/null | awk '{gsub(/:/, \"\", $2); print $2,$9}' || true", ); const addrs = ifconfigOut.stdout @@ -50,32 +93,41 @@ export async function collectNetworkMetrics(client: Client): Promise<{ const parts = line.split(/\s+/); if (parts.length >= 2) { const name = parts[0]; + if (name === "lo") continue; const state = parts[1]; const existing = ifMap.get(name); if (existing) { existing.state = state; + } else { + ifMap.set(name, { ip: "", state }); } } } try { + const firstReadAt = Date.now(); const procNet = await execCommand(client, "cat /proc/net/dev"); - const rxTxMap = new Map(); - for (const line of procNet.stdout.split("\n").slice(2)) { - const parts = line.trim().split(/\s+/); - if (parts.length >= 10) { - const ifName = parts[0].replace(":", ""); - rxTxMap.set(ifName, { rx: parts[1], tx: parts[9] }); + await new Promise((resolve) => setTimeout(resolve, 500)); + const procNetAfter = await execCommand(client, "cat /proc/net/dev"); + const elapsedSeconds = (Date.now() - firstReadAt) / 1000; + const rxTxMap = parseNetworkCounters(procNet.stdout); + const afterMap = parseNetworkCounters(procNetAfter.stdout); + if (ifMap.size === 0) { + for (const name of rxTxMap.keys()) { + if (name !== "lo") ifMap.set(name, { ip: "", state: "UNKNOWN" }); } } for (const [name, data] of ifMap.entries()) { const rxTx = rxTxMap.get(name); + const after = afterMap.get(name); interfaces.push({ name, ip: data.ip, state: data.state, rxBytes: rxTx?.rx ?? null, txBytes: rxTx?.tx ?? null, + rxRateBps: counterRate(rxTx?.rx, after?.rx, elapsedSeconds), + txRateBps: counterRate(rxTx?.tx, after?.tx, elapsedSeconds), }); } } catch { @@ -86,6 +138,8 @@ export async function collectNetworkMetrics(client: Client): Promise<{ state: data.state, rxBytes: null, txBytes: null, + rxRateBps: null, + txRateBps: null, }); } } diff --git a/src/backend/hosts/metrics/widgets/processes-collector.ts b/src/backend/hosts/metrics/widgets/processes-collector.ts index cde0626..63d3c05 100644 --- a/src/backend/hosts/metrics/widgets/processes-collector.ts +++ b/src/backend/hosts/metrics/widgets/processes-collector.ts @@ -23,7 +23,10 @@ export async function collectProcessesMetrics(client: Client): Promise<{ }> = []; try { - const psOut = await execCommand(client, "ps aux --sort=-%cpu | head -n 11"); + const psOut = await execCommand( + client, + "(ps aux --sort=-%cpu 2>/dev/null || ps aux) | head -n 11", + ); const psLines = psOut.stdout .split("\n") .map((l) => l.trim()) diff --git a/src/backend/hosts/opkssh-auth.ts b/src/backend/hosts/opkssh-auth.ts index 1988bc9..9bbbbf2 100644 --- a/src/backend/hosts/opkssh-auth.ts +++ b/src/backend/hosts/opkssh-auth.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../utils/error-message.js"; import { spawn, ChildProcess } from "child_process"; import { randomUUID } from "crypto"; import { WebSocket } from "ws"; @@ -26,11 +27,7 @@ interface OPKSSHAuthSession { remoteRedirectUri: string; providers: Array<{ alias: string; issuer: string }>; status: - | "starting" - | "waiting_for_auth" - | "authenticating" - | "completed" - | "error"; + "starting" | "waiting_for_auth" | "authenticating" | "completed" | "error"; ws: WebSocket; stdoutBuffer: string; privateKeyBuffer: string; @@ -523,7 +520,7 @@ export async function startOPKSSHAuth( JSON.stringify({ type: "opkssh_error", requestId, - error: `Failed to start OPKSSH authentication: ${error instanceof Error ? error.message : "Unknown error"}`, + error: `Failed to start OPKSSH authentication: ${getErrorMessage(error)}`, }), ); return ""; diff --git a/src/backend/hosts/opkssh-cert-auth.ts b/src/backend/hosts/opkssh-cert-auth.ts index 6b8eb65..e9aae0b 100644 --- a/src/backend/hosts/opkssh-cert-auth.ts +++ b/src/backend/hosts/opkssh-cert-auth.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../utils/error-message.js"; // SSH certificate authentication workarounds for ssh2. // ssh2 doesn't support OpenSSH cert auth natively โ€” this module grafts // the certificate onto the parsed key, wraps ECDSA signing to convert @@ -357,7 +358,7 @@ export async function setupCACertAuth( const parsed = passphrase ? parseKey(keyBuf, passphrase) : parseKey(keyBuf); if (parsed instanceof Error || !parsed) { - const errMsg = parsed instanceof Error ? parsed.message : "unknown error"; + const errMsg = getErrorMessage(parsed, "unknown error"); throw new Error(`Failed to parse private key for CA cert auth: ${errMsg}`); } const privKey = ( diff --git a/src/backend/hosts/proxmox-shared.ts b/src/backend/hosts/proxmox-shared.ts new file mode 100644 index 0000000..112c9f0 --- /dev/null +++ b/src/backend/hosts/proxmox-shared.ts @@ -0,0 +1,7 @@ +// Proxmox node names are restricted to [a-zA-Z0-9-] by PVE itself, +// but we validate defensively before using in a shell command. +const SAFE_NODE_RE = /^[a-zA-Z0-9._-]{1,64}$/; + +export function isSafeNodeName(name: string): boolean { + return SAFE_NODE_RE.test(name); +} diff --git a/src/backend/hosts/serial.ts b/src/backend/hosts/serial.ts index 7868c2e..a03a25e 100644 --- a/src/backend/hosts/serial.ts +++ b/src/backend/hosts/serial.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../utils/error-message.js"; import { WebSocketServer, WebSocket, type RawData } from "ws"; import { SerialPort } from "serialport"; import { AuthManager } from "../utils/auth-manager.js"; @@ -21,6 +22,12 @@ const authManager = AuthManager.getInstance(); const wss = new WebSocketServer({ port: 30011 }); +wss.on("error", (error) => { + sshLogger.error("Serial WebSocket server error", error, { + operation: "wss_error", + }); +}); + wss.on("connection", async (ws: WebSocket, req) => { let userId: string | undefined; @@ -103,7 +110,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } catch (err) { send({ type: "error", - data: err instanceof Error ? err.message : "Failed to list ports", + data: getErrorMessage(err, "Failed to list ports"), }); } break; @@ -166,8 +173,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } catch (err) { send({ type: "error", - data: - err instanceof Error ? err.message : "Failed to open serial port", + data: getErrorMessage(err, "Failed to open serial port"), }); } break; diff --git a/src/backend/hosts/session-sharing/routes.ts b/src/backend/hosts/session-sharing/routes.ts new file mode 100644 index 0000000..d878236 --- /dev/null +++ b/src/backend/hosts/session-sharing/routes.ts @@ -0,0 +1,538 @@ +import crypto from "crypto"; +import express, { type Request, type Response } from "express"; +import type { AuthenticatedRequest } from "../../../types/index.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { PermissionManager } from "../../utils/permission-manager.js"; +import { sshLogger } from "../../utils/logger.js"; +import { sessionManager } from "../terminal/session-manager.js"; +import { getGuacSessionInfo } from "../guacamole/guacamole-server.js"; +import { GuacamoleTokenService } from "../guacamole/token-service.js"; +import { + createCurrentSessionShareRepository, + createCurrentSettingsRepository, + createCurrentHostResolutionRepository, +} from "../../database/repositories/factory.js"; + +const router = express.Router(); +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); +const permissionManager = PermissionManager.getInstance(); +const tokenService = GuacamoleTokenService.getInstance(); + +const DEFAULT_EXPIRY_HOURS = 24; +const MAX_EXPIRY_HOURS = 24 * 30; + +type Protocol = "ssh" | "rdp" | "vnc" | "telnet"; +type PermissionLevel = "read-only" | "read-write"; + +interface ResolveRateEntry { + count: number; + windowStart: number; +} +const resolveAttempts = new Map(); +const RESOLVE_WINDOW_MS = 60 * 1000; +const RESOLVE_MAX_ATTEMPTS = 30; + +function isResolveRateLimited(ip: string): boolean { + const now = Date.now(); + const entry = resolveAttempts.get(ip); + if (!entry || now - entry.windowStart > RESOLVE_WINDOW_MS) { + resolveAttempts.set(ip, { count: 1, windowStart: now }); + return false; + } + entry.count += 1; + return entry.count > RESOLVE_MAX_ATTEMPTS; +} + +setInterval( + () => { + const now = Date.now(); + for (const [ip, entry] of resolveAttempts.entries()) { + if (now - entry.windowStart > RESOLVE_WINDOW_MS) + resolveAttempts.delete(ip); + } + }, + 5 * 60 * 1000, +); + +async function isSharingEnabledForHost(hostId: number): Promise<{ + enabled: boolean; + hostOwnerId: string | null; +}> { + const globalEnabled = await createCurrentSettingsRepository().getBoolean( + "session_sharing_globally_enabled", + true, + ); + if (!globalEnabled) return { enabled: false, hostOwnerId: null }; + + const hostResolutionRepository = createCurrentHostResolutionRepository(); + const hostOwnerId = await hostResolutionRepository.findHostOwnerId(hostId); + if (!hostOwnerId) return { enabled: false, hostOwnerId: null }; + + const host = await hostResolutionRepository.findHostById(hostId, hostOwnerId); + if (!host) return { enabled: false, hostOwnerId: null }; + + return { + enabled: host.allowSessionSharing !== false, + hostOwnerId, + }; +} + +function computeExpiresAt(expiryHours: number | undefined): string { + const hours = Math.min( + Math.max(expiryHours ?? DEFAULT_EXPIRY_HOURS, 1), + MAX_EXPIRY_HOURS, + ); + return new Date(Date.now() + hours * 60 * 60 * 1000).toISOString(); +} + +function isLiveSessionOwnedBy( + protocol: Protocol, + sessionId: string, + userId: string, +): boolean { + if (protocol === "ssh") { + const session = sessionManager.getSession(sessionId); + return !!session && session.isConnected && session.userId === userId; + } + const info = getGuacSessionInfo(sessionId); + return !!info && info.ownerUserId === userId; +} + +function isLiveSession(protocol: Protocol, sessionId: string): boolean { + if (protocol === "ssh") { + const session = sessionManager.getSession(sessionId); + return !!session && session.isConnected; + } + return !!getGuacSessionInfo(sessionId); +} + +/** + * @openapi + * /session-sharing/create: + * post: + * summary: Create a session share (link or targeted user) + * description: Mints a share grant for a live terminal/RDP/VNC/Telnet session. Caller must own the live session. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - hostId + * - sessionId + * - protocol + * - shareType + * - permissionLevel + * properties: + * hostId: + * type: integer + * sessionId: + * type: string + * tabInstanceId: + * type: string + * protocol: + * type: string + * enum: [ssh, rdp, vnc, telnet] + * shareType: + * type: string + * enum: [link, user] + * targetUserId: + * type: string + * permissionLevel: + * type: string + * enum: [read-only, read-write] + * expiryHours: + * type: number + * responses: + * 200: + * description: Share created + * 400: + * description: Invalid request + * 403: + * description: Sharing disabled, or caller does not own the session + * 500: + * description: Server error + */ +router.post("/create", authenticateJWT, async (req: Request, res: Response) => { + try { + const userId = (req as AuthenticatedRequest).userId!; + const { + hostId, + sessionId, + tabInstanceId, + protocol, + shareType, + targetUserId, + permissionLevel, + expiryHours, + } = req.body ?? {}; + + if (!hostId || !sessionId || !protocol || !shareType || !permissionLevel) { + return res.status(400).json({ error: "Missing required fields" }); + } + if (!["ssh", "rdp", "vnc", "telnet"].includes(protocol)) { + return res.status(400).json({ error: "Invalid protocol" }); + } + if (!["link", "user"].includes(shareType)) { + return res.status(400).json({ error: "Invalid shareType" }); + } + if (!["read-only", "read-write"].includes(permissionLevel)) { + return res.status(400).json({ error: "Invalid permissionLevel" }); + } + if (shareType === "user" && !targetUserId) { + return res + .status(400) + .json({ error: "targetUserId is required for user shares" }); + } + + const numericHostId = Number(hostId); + + const { enabled: sharingEnabled } = + await isSharingEnabledForHost(numericHostId); + if (!sharingEnabled) { + return res + .status(403) + .json({ error: "Session sharing is disabled for this host" }); + } + + if (!isLiveSessionOwnedBy(protocol, String(sessionId), userId)) { + return res + .status(403) + .json({ error: "You do not own this live session" }); + } + + if (shareType === "user") { + const accessInfo = await permissionManager.canAccessHost( + targetUserId, + numericHostId, + "connect", + ); + if (!accessInfo.hasAccess) { + return res.status(403).json({ + error: "Target user does not have access to this host", + }); + } + } + + const shareId = crypto.randomUUID(); + const linkToken = + shareType === "link" + ? crypto.randomBytes(24).toString("base64url") + : null; + const expiresAt = computeExpiresAt(expiryHours); + + const created = await createCurrentSessionShareRepository().create({ + id: shareId, + hostId: numericHostId, + ownerUserId: userId, + protocol, + sessionId: String(sessionId), + tabInstanceId: tabInstanceId ?? null, + shareType, + targetUserId: shareType === "user" ? targetUserId : null, + linkToken, + permissionLevel, + expiresAt, + }); + + res.json({ + shareId: created.id, + linkToken: created.linkToken, + expiresAt: created.expiresAt, + }); + } catch (error) { + sshLogger.error("Failed to create session share", error, { + operation: "session_share_create_error", + }); + res.status(500).json({ error: "Failed to create session share" }); + } +}); + +/** + * @openapi + * /session-sharing/host/{hostId}/active: + * get: + * summary: List active session shares for a host + * description: Returns active (non-revoked, non-expired) shares owned by the caller for the given host. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: hostId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: List of active shares + * 400: + * description: Invalid host id + * 500: + * description: Server error + */ +router.get( + "/host/:hostId/active", + authenticateJWT, + async (req: Request, res: Response) => { + try { + const userId = (req as AuthenticatedRequest).userId!; + const hostId = Number.parseInt(String(req.params.hostId), 10); + if (!hostId || Number.isNaN(hostId)) { + return res.status(400).json({ error: "Invalid host ID" }); + } + + const shares = + await createCurrentSessionShareRepository().findActiveSharesForHost( + hostId, + userId, + ); + + res.json({ shares }); + } catch (error) { + sshLogger.error("Failed to list session shares", error, { + operation: "session_share_list_error", + }); + res.status(500).json({ error: "Failed to list session shares" }); + } + }, +); + +/** + * @openapi + * /session-sharing/{shareId}: + * delete: + * summary: Revoke a session share + * description: Revokes a share. Owner or admin only. Best-effort kick of live SSH participants; guac joins are not force-disconnected in v1. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: shareId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Share revoked + * 403: + * description: Not authorized to revoke this share + * 404: + * description: Share not found + * 500: + * description: Server error + */ +router.delete( + "/:shareId", + authenticateJWT, + async (req: Request, res: Response) => { + try { + const userId = (req as AuthenticatedRequest).userId!; + const shareId = String(req.params.shareId); + + const repository = createCurrentSessionShareRepository(); + const share = await repository.findById(shareId); + if (!share) { + return res.status(404).json({ error: "Share not found" }); + } + + let revoked = await repository.revoke(shareId, userId); + if (!revoked) { + if (await permissionManager.isAdmin(userId)) { + revoked = await repository.revokeAsAdmin(shareId); + } + } + + if (!revoked) { + return res + .status(403) + .json({ error: "Not authorized to revoke this share" }); + } + + // Best-effort kick of live participants. SSH sessions support ending + // just the guests via ownerEndSession; guac joins aren't force-kickable + // from a REST handler (guacamole-lite exposes no kick API), so a revoked + // guac link only blocks *future* resolves until the guest's own socket ends. + if (share.protocol === "ssh") { + try { + sessionManager.ownerEndSession( + share.sessionId, + "Session share revoked by owner", + ); + } catch { + // best-effort only + } + } + + res.json({ success: true }); + } catch (error) { + sshLogger.error("Failed to revoke session share", error, { + operation: "session_share_revoke_error", + }); + res.status(500).json({ error: "Failed to revoke session share" }); + } + }, +); + +/** + * @openapi + * /session-sharing/resolve/{linkToken}: + * get: + * summary: Resolve a guest share link + * description: Public, unauthenticated endpoint for anonymous share-link guests. Never returns host name, IP, username, or hostId. Rate-limited per IP. + * tags: + * - Session Sharing + * parameters: + * - in: path + * name: linkToken + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Resolved share connection info + * 404: + * description: Link not found, expired, revoked, or sharing disabled + * 429: + * description: Too many requests + * 500: + * description: Server error + */ +router.get("/resolve/:linkToken", async (req: Request, res: Response) => { + try { + const ip = req.ip || req.socket.remoteAddress || "unknown"; + if (isResolveRateLimited(ip)) { + return res.status(429).json({ error: "Too many requests" }); + } + + const linkToken = String(req.params.linkToken); + const repository = createCurrentSessionShareRepository(); + const share = await repository.findByLinkToken(linkToken); + if (!share) { + return res.status(404).json({ error: "Link not found or expired" }); + } + + const { enabled: sharingEnabled } = await isSharingEnabledForHost( + share.hostId, + ); + if (!sharingEnabled) { + return res.status(404).json({ error: "Link not found or expired" }); + } + + const protocol = share.protocol as Protocol; + if (!isLiveSession(protocol, share.sessionId)) { + return res.status(404).json({ error: "Session is no longer active" }); + } + + // Field-by-field by design - never spread a host row into this response. + // Anonymous guests must never see hostname/IP/username/hostId (decision #5). + const response: { + protocol: Protocol; + permissionLevel: PermissionLevel; + wsPath: string; + connectParams?: Record; + } = { + protocol, + permissionLevel: share.permissionLevel as PermissionLevel, + wsPath: + protocol === "ssh" + ? `/terminal/ws?shareToken=${encodeURIComponent(linkToken)}` + : "/guacamole/websocket/", + }; + + if (protocol !== "ssh") { + const joinToken = tokenService.createJoinToken( + share.sessionId, + share.permissionLevel === "read-only", + ); + response.connectParams = { token: joinToken }; + } + + try { + await repository.touchShareUsage(share.id); + await repository.recordParticipantJoin(share.id, null, "Guest"); + } catch { + // best-effort, never fail the resolve response over audit bookkeeping + } + + res.json(response); + } catch (error) { + sshLogger.error("Failed to resolve session share link", error, { + operation: "session_share_resolve_error", + }); + res.status(500).json({ error: "Failed to resolve share link" }); + } +}); + +/** + * @openapi + * /session-sharing/{shareId}/end: + * post: + * summary: End a shared session for all participants + * description: Owner-only. Terminates the underlying session and notifies joined participants. Guac protocol kick is best-effort in v1. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: shareId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Session ended + * 403: + * description: Not the owner of this share + * 404: + * description: Share not found + * 500: + * description: Server error + */ +router.post( + "/:shareId/end", + authenticateJWT, + async (req: Request, res: Response) => { + try { + const userId = (req as AuthenticatedRequest).userId!; + const shareId = String(req.params.shareId); + + const repository = createCurrentSessionShareRepository(); + const share = await repository.findById(shareId); + if (!share) { + return res.status(404).json({ error: "Share not found" }); + } + if (share.ownerUserId !== userId) { + return res.status(403).json({ error: "Not the owner of this share" }); + } + + if (share.protocol === "ssh") { + sessionManager.ownerEndSession( + share.sessionId, + "Session ended by owner", + ); + } + // Guac protocols: no kick API available from a REST handler in v1 - see + // DELETE /:shareId for the same limitation. + + res.json({ success: true }); + } catch (error) { + sshLogger.error("Failed to end shared session", error, { + operation: "session_share_end_error", + }); + res.status(500).json({ error: "Failed to end shared session" }); + } + }, +); + +export default router; diff --git a/src/backend/hosts/ssh-client-factory.ts b/src/backend/hosts/ssh-client-factory.ts new file mode 100644 index 0000000..040be98 --- /dev/null +++ b/src/backend/hosts/ssh-client-factory.ts @@ -0,0 +1,223 @@ +import { getErrorMessage } from "../utils/error-message.js"; +import { Client, type ConnectConfig } from "ssh2"; +import type { SSHHost } from "../../types/index.js"; +import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; +import { preparePrivateKeyForSSH2 } from "../utils/ssh-key-utils.js"; +import { + createSocks5Connection, + type SOCKS5Config, +} from "../utils/socks5-helper.js"; +import { SSHHostKeyVerifier } from "./host-key-verifier.js"; +import { createJumpHostChain } from "./jump-host-chain.js"; +import { resolveSshConnectConfigHost } from "./ssh-dns.js"; + +/** + * Non-interactive SSH connection helpers shared by fleet execution. + * + * Mirrors buildSshConfig/createSshFactory/getPoolKey in hosts/metrics/index.ts, + * minus the keyboard-interactive TOTP prompt handling (metrics/terminal-only) + * and OPKSSH/Vault cert setup (not supported as fleet auth for v1 - those flows + * need an interactive browser step that has no place in a batch fleet run). + */ + +export function getFleetPoolKey(host: SSHHost): string { + const socks5Key = host.useSocks5 + ? `:socks5:${host.socks5Host}:${host.socks5Port}` + : ""; + return `fleet:${host.userId}:${host.ip}:${host.port}:${host.username}${socks5Key}`; +} + +export async function buildFleetSshConfig( + host: SSHHost, +): Promise { + const base: ConnectConfig = { + host: host.ip?.replace(/^\[|\]$/g, "") || host.ip, + port: host.port, + username: host.username, + keepaliveInterval: 30000, + keepaliveCountMax: 3, + readyTimeout: 30000, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 30000, + hostVerifier: await SSHHostKeyVerifier.createHostVerifier( + host.id, + host.ip, + host.port, + null, + host.userId || "", + false, + ), + env: { + TERM: "xterm-256color", + LANG: "en_US.UTF-8", + LC_ALL: "en_US.UTF-8", + }, + algorithms: { + kex: [ + "curve25519-sha256", + "curve25519-sha256@libssh.org", + "ecdh-sha2-nistp521", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp256", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group14-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group1-sha1", + ], + serverHostKey: [ + "ssh-ed25519", + "ecdsa-sha2-nistp521", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp256", + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + "ssh-dss", + ], + cipher: SSH_ALGORITHMS.cipher, + hmac: [ + "hmac-sha2-512-etm@openssh.com", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512", + "hmac-sha2-256", + "hmac-sha1", + "hmac-md5", + ], + compress: ["none", "zlib@openssh.com", "zlib"], + }, + } as ConnectConfig; + + const authType = host.authType; + + if (authType === "password" || authType === "credential") { + if (!host.password) { + throw new Error(`No password available for host ${host.ip}`); + } + base.password = host.password; + } else if (authType === "key") { + if (!host.key) { + throw new Error(`No SSH key available for host ${host.ip}`); + } + (base as Record).privateKey = preparePrivateKeyForSSH2( + host.key, + host.keyPassword, + ); + if (host.keyPassword) { + (base as Record).passphrase = host.keyPassword; + } + } else if (authType === "none" || authType === "tailscale") { + // no credentials needed + } else { + throw new Error( + `Unsupported authentication type '${authType}' for fleet execution on host ${host.ip}`, + ); + } + + return base; +} + +export function createFleetSshFactory(host: SSHHost): () => Promise { + return async () => { + const config = await buildFleetSshConfig(host); + const client = new Client(); + + const proxyConfig: SOCKS5Config | null = + host.useSocks5 && + (host.socks5Host || + (host.socks5ProxyChain && host.socks5ProxyChain.length > 0)) + ? { + useSocks5: host.useSocks5, + socks5Host: host.socks5Host, + socks5Port: host.socks5Port, + socks5Username: host.socks5Username, + socks5Password: host.socks5Password, + socks5ProxyChain: host.socks5ProxyChain, + } + : null; + + const hasJumpHosts = + host.jumpHosts && host.jumpHosts.length > 0 && host.userId; + + let jumpClient: Client | null = null; + if (hasJumpHosts) { + jumpClient = await createJumpHostChain(host.jumpHosts!, host.userId!); + if (!jumpClient) { + throw new Error("Failed to establish jump host chain"); + } + } else if (proxyConfig) { + try { + const proxySocket = await createSocks5Connection( + host.ip, + host.port, + proxyConfig, + ); + if (proxySocket) { + config.sock = proxySocket; + } + } catch (proxyError) { + throw new Error( + "Proxy connection failed: " + getErrorMessage(proxyError), + { cause: proxyError }, + ); + } + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + client.end(); + jumpClient?.end(); + reject(new Error("SSH connection timeout")); + }, 30000); + + client.on("ready", () => { + clearTimeout(timeout); + resolve(client); + }); + + client.on("close", () => { + jumpClient?.end(); + }); + + client.on("error", (err) => { + clearTimeout(timeout); + jumpClient?.end(); + reject(err); + }); + + if (jumpClient) { + jumpClient.forwardOut( + "127.0.0.1", + 0, + host.ip, + host.port, + (err, stream) => { + if (err) { + clearTimeout(timeout); + jumpClient!.end(); + reject( + new Error( + "Failed to forward through jump host: " + err.message, + ), + ); + return; + } + config.sock = stream; + client.connect(config); + }, + ); + } else if (config.sock) { + client.connect(config); + } else { + resolveSshConnectConfigHost(config) + .then(() => { + client.connect(config); + }) + .catch((error) => { + clearTimeout(timeout); + reject(error); + }); + } + }); + }; +} diff --git a/src/backend/hosts/ssh-keepalive.test.ts b/src/backend/hosts/ssh-keepalive.test.ts new file mode 100644 index 0000000..cd7733a --- /dev/null +++ b/src/backend/hosts/ssh-keepalive.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { resolveSshKeepalive } from "./ssh-keepalive.js"; + +describe("resolveSshKeepalive", () => { + it("preserves zero to disable SSH keepalives", () => { + expect(resolveSshKeepalive(0, 0, 30000, 5)).toEqual({ + keepaliveInterval: 0, + keepaliveCountMax: 0, + }); + }); + + it("uses defaults when keepalive settings are absent", () => { + expect(resolveSshKeepalive(undefined, undefined, 60000, 5)).toEqual({ + keepaliveInterval: 60000, + keepaliveCountMax: 5, + }); + }); + + it("enforces the existing minimums for positive settings", () => { + expect(resolveSshKeepalive(1, 0.5, 30000, 5)).toEqual({ + keepaliveInterval: 5000, + keepaliveCountMax: 1, + }); + }); +}); diff --git a/src/backend/hosts/ssh-keepalive.ts b/src/backend/hosts/ssh-keepalive.ts new file mode 100644 index 0000000..e85603d --- /dev/null +++ b/src/backend/hosts/ssh-keepalive.ts @@ -0,0 +1,23 @@ +const MIN_KEEPALIVE_INTERVAL_MS = 5000; + +export function resolveSshKeepalive( + intervalSeconds: number | undefined, + countMax: number | undefined, + defaultIntervalMs: number, + defaultCountMax: number, +) { + return { + keepaliveInterval: + typeof intervalSeconds !== "number" + ? defaultIntervalMs + : intervalSeconds === 0 + ? 0 + : Math.max(MIN_KEEPALIVE_INTERVAL_MS, intervalSeconds * 1000), + keepaliveCountMax: + typeof countMax !== "number" + ? defaultCountMax + : countMax === 0 + ? 0 + : Math.max(1, countMax), + }; +} diff --git a/src/backend/hosts/tailscale-check.ts b/src/backend/hosts/tailscale-check.ts new file mode 100644 index 0000000..5df26b8 --- /dev/null +++ b/src/backend/hosts/tailscale-check.ts @@ -0,0 +1,42 @@ +// Tailscale SSH "check mode" sends its re-authentication prompt as an SSH auth +// banner during the "none" auth method, then long-polls its control plane while +// the connection stays open. The banner text itself comes from the control plane +// (it is not in the tailscaled source), so match on the login URL rather than the +// surrounding wording, which can change without notice. + +const TAILSCALE_CHECK_URL = /https:\/\/login\.tailscale\.com\/a\/[A-Za-z0-9]+/; + +const CHECK_COMPLETE = /authentication checked/i; + +export interface TailscaleCheckBanner { + url: string; + message: string; +} + +function stripCommentMarkers(banner: string): string { + return banner + .split(/\r?\n/) + .map((line) => line.replace(/^\s*#\s?/, "").trim()) + .filter((line) => line.length > 0) + .join("\n") + .trim(); +} + +export function parseTailscaleCheckBanner( + banner: string, +): TailscaleCheckBanner | null { + if (!banner) return null; + + const match = banner.match(TAILSCALE_CHECK_URL); + if (!match) return null; + + return { + url: match[0], + message: stripCommentMarkers(banner), + }; +} + +export function isTailscaleCheckCompleteBanner(banner: string): boolean { + if (!banner) return false; + return CHECK_COMPLETE.test(banner); +} diff --git a/src/backend/hosts/terminal/host-identity.ts b/src/backend/hosts/terminal/host-identity.ts new file mode 100644 index 0000000..74f649a --- /dev/null +++ b/src/backend/hosts/terminal/host-identity.ts @@ -0,0 +1,77 @@ +/** + * Comparable form of a host address: bracketed IPv6 literals and hostname + * casing are presentation, not identity. + */ +export function normalizeHostAddress(value: unknown): string { + if (typeof value !== "string") return ""; + return value + .replace(/^\[|\]$/g, "") + .trim() + .toLowerCase(); +} + +/** + * Whether a host id resolved to a different machine than the client meant. + * + * A client addresses a host by the numeric row id of the database it is + * displaying. When the desktop app delegates a connection to a remote sync + * server, the id is resolved against *that* server's table instead, and + * autoincrement ids need not line up between the two โ€” they diverge as soon as + * the sides accumulate inserts and deletes in a different order. + * + * The resolved row then supplies the address, the credentials, the jump hosts + * and the stored host key, so a mismatch opens an interactive shell on a + * machine the user did not pick. + * + * Returns false when the server has no address to compare: the caller falls + * back to what the client supplied, which is the behaviour of every setup that + * never stored the host server-side. + */ +export function hostAddressMismatch( + clientAddress: unknown, + resolvedAddress: unknown, +): boolean { + const resolved = normalizeHostAddress(resolvedAddress); + if (!resolved) return false; + + return resolved !== normalizeHostAddress(clientAddress); +} + +/** + * Shown to the user on every path that refuses a mismatch, so the wording of + * the one thing they can act on does not depend on which feature they used. + */ +export const HOST_ADDRESS_MISMATCH_MESSAGE = + "Host mismatch: this server resolved the selected host to a different machine, so the connection was refused. " + + "The host ids on this device and on the sync server have drifted apart. " + + 'Set the connection origin to "This device" for this host, or re-run a full sync, then try again.'; + +/** + * Shown when the client named a host by sync identity that this server does + * not have. Distinct from a mismatch: nothing was resolved at all, so the + * remedy is to sync the host across rather than to pick a different origin. + */ +export const HOST_NOT_ON_THIS_SERVER_MESSAGE = + "This host does not exist on the sync server, so the connection was refused. " + + 'Run a sync so the server knows about it, or set the connection origin to "This device" for this host.'; + +/** + * Thrown where a mismatch is reported by rejecting rather than by messaging + * the socket. Callers whose host-resolution is wrapped in a "failed to resolve + * credentials, carry on" catch must let this one through: continuing is the + * behaviour being prevented. + */ +export class HostAddressMismatchError extends Error { + constructor() { + super(HOST_ADDRESS_MISMATCH_MESSAGE); + this.name = "HostAddressMismatchError"; + } +} + +/** Counterpart of {@link HostAddressMismatchError} for an unknown sync id. */ +export class HostNotOnThisServerError extends Error { + constructor() { + super(HOST_NOT_ON_THIS_SERVER_MESSAGE); + this.name = "HostNotOnThisServerError"; + } +} diff --git a/src/backend/hosts/terminal/index.ts b/src/backend/hosts/terminal/index.ts index f23b2a1..526db5c 100644 --- a/src/backend/hosts/terminal/index.ts +++ b/src/backend/hosts/terminal/index.ts @@ -1,3 +1,5 @@ +import { getErrorMessage } from "../../utils/error-message.js"; +import { StringDecoder } from "string_decoder"; import { WebSocketServer, WebSocket, type RawData } from "ws"; import ssh2Pkg, { type Client as SSHClientType, @@ -20,7 +22,18 @@ import { SSHAuthManager } from "../auth-manager.js"; import type { ProxyNode } from "../../../types/index.js"; import { SSHHostKeyVerifier } from "../host-key-verifier.js"; import { createJumpHostChain } from "../jump-host-chain.js"; -import { sessionManager } from "./session-manager.js"; +import { + parseTailscaleCheckBanner, + isTailscaleCheckCompleteBanner, +} from "../tailscale-check.js"; +import { + sessionManager, + isMessageAllowedForParticipant, +} from "./session-manager.js"; +import { + createCurrentSessionShareRepository, + createCurrentSettingsRepository, +} from "../../database/repositories/factory.js"; import { detectTmux, attachOrCreateTmuxSession, @@ -34,13 +47,22 @@ import { import { isWindowsSftpPath, sftpPathToLocalPath } from "../transfer-paths.js"; import { preparePrivateKeyForSSH2 } from "../../utils/ssh-key-utils.js"; import { triggerLoginAlert } from "../../utils/alert-trigger.js"; +import { getClientIp } from "../../utils/request-origin.js"; import { isRetriableDnsError, resolveHostForSshConnect } from "../ssh-dns.js"; +import { resolveSshKeepalive } from "../ssh-keepalive.js"; +import { + hostAddressMismatch, + HOST_ADDRESS_MISMATCH_MESSAGE, + HOST_NOT_ON_THIS_SERVER_MESSAGE, +} from "./host-identity.js"; interface ConnectToHostData { cols: number; rows: number; hostConfig: { id: number; + /** Names the host across a sync pair; `id` only names it locally. */ + syncId?: string | null; instanceId?: string; ip: string; port: number; @@ -99,16 +121,182 @@ interface WebSocketMessage { const authManager = AuthManager.getInstance(); +// Tailscale holds a check-mode connection open for up to 30 minutes while the +// user completes the browser login, so match that rather than timing out first. +const TAILSCALE_CHECK_TIMEOUT_MS = 1_800_000; + const userConnections = new Map>(); const wss = new WebSocketServer({ port: 30002, }); +wss.on("error", (error) => { + sshLogger.error("WebSocket server error", error, { + operation: "wss_error", + }); +}); + +/** + * Auth path for anonymous share-link guests (?shareToken=). + * Never touches DataCrypto/user credentials - guests join an already-live + * stream and never decrypt stored secrets. + */ +async function handleShareTokenConnection( + ws: WebSocket, + req: import("http").IncomingMessage, + shareToken: string, +): Promise { + const shareRepo = createCurrentSessionShareRepository(); + const share = await shareRepo.findByLinkToken(shareToken); + if (!share) { + ws.close(1008, "Invalid or expired share link"); + return; + } + if (share.protocol !== "ssh") { + ws.close(1008, "Unsupported share protocol"); + return; + } + + const globallyEnabled = await createCurrentSettingsRepository().getBoolean( + "session_sharing_globally_enabled", + true, + ); + if (!globallyEnabled) { + ws.close(1008, "Session sharing is disabled"); + return; + } + + const host = await createCurrentHostResolutionRepository().findHostById( + share.hostId, + share.ownerUserId, + ); + if (!host || host.allowSessionSharing === false) { + ws.close(1008, "Session sharing is disabled for this host"); + return; + } + + const session = sessionManager.getSession(share.sessionId); + if (!session || !session.isConnected) { + ws.close(1008, "Session has ended"); + return; + } + + const permissionLevel = share.permissionLevel as "read-write" | "read-only"; + const joined = sessionManager.joinAsParticipant(share.sessionId, ws, { + userId: null, + permissionLevel, + guestLabel: "Guest", + shareId: share.id, + }); + if (!joined) { + ws.close(1008, "Session is no longer active"); + return; + } + + shareRepo.touchShareUsage(share.id).catch(() => {}); + shareRepo.recordParticipantJoin(share.id, null, "Guest").catch(() => {}); + + const buffered = sessionManager.getBuffer(joined); + if (buffered) { + ws.send(JSON.stringify({ type: "data", data: buffered })); + } + ws.send( + JSON.stringify({ type: "sessionAttached", sessionId: share.sessionId }), + ); + ws.send(JSON.stringify({ type: "connected", message: "Joined session" })); + + const currentSessionId: string = share.sessionId; + + let wsAlive = true; + ws.on("pong", () => { + wsAlive = true; + }); + const wsPingInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + if (!wsAlive) { + ws.terminate(); + return; + } + wsAlive = false; + ws.ping(); + } else { + clearInterval(wsPingInterval); + } + }, 30000); + + ws.on("close", () => { + clearInterval(wsPingInterval); + sessionManager.removeParticipant(currentSessionId, ws); + sshLogger.info("Guest left shared terminal session", { + operation: "terminal_guest_disconnect", + sessionId: currentSessionId, + shareId: share.id, + }); + }); + + ws.on("message", (msg: RawData) => { + let parsed: WebSocketMessage; + try { + parsed = JSON.parse(msg.toString()) as WebSocketMessage; + } catch { + return; + } + const { type, data } = parsed; + + const liveSession = sessionManager.getSession(currentSessionId); + const participant = liveSession + ? sessionManager.getParticipantForWs(liveSession, ws) + : null; + if (!isMessageAllowedForParticipant(participant, type)) { + return; + } + + switch (type) { + case "input": { + const inputData = data as string; + sessionManager.bufferInput(currentSessionId, inputData); + const inputStream = liveSession?.sshStream; + if (inputStream) { + try { + inputStream.write(Buffer.from(inputData, "utf8")); + } catch { + inputStream.write(Buffer.from(inputData, "latin1")); + } + } + break; + } + case "ping": + ws.send(JSON.stringify({ type: "pong" })); + break; + case "disconnect": + sessionManager.removeParticipant(currentSessionId, ws); + break; + default: + break; + } + }); +} + wss.on("connection", async (ws: WebSocket, req) => { let userId: string | undefined; let sessionId: string | undefined; + ws.on("error", (error) => { + sshLogger.error("WebSocket connection error", error, { + operation: "ws_error", + sessionId, + }); + }); + + const urlObj = new URL(req.url || "", "http://localhost"); + const shareToken = urlObj.searchParams.get("shareToken"); + + if (shareToken) { + await handleShareTokenConnection(ws, req, shareToken); + return; + } + try { let token: string | undefined; @@ -126,7 +314,6 @@ wss.on("connection", async (ws: WebSocket, req) => { } if (!token) { - const urlObj = new URL(req.url || "", "http://localhost"); const qp = urlObj.searchParams.get("token"); if (qp) token = qp; } @@ -150,7 +337,7 @@ wss.on("connection", async (ws: WebSocket, req) => { error, { operation: "websocket_connection_auth_error", - ip: req.socket.remoteAddress, + ip: getClientIp(req), }, ); ws.close(1008, "Authentication required"); @@ -242,11 +429,20 @@ wss.on("connection", async (ws: WebSocket, req) => { if (currentSessionId) { const session = sessionManager.getSession(currentSessionId); if (session?.isConnected) { - // Only detach if this WS is still the one attached to the session. - // If a refresh reconnected and reattached a new WS before this close - // event fired, we must not clobber that new attachment. - if (session.attachedWs === ws || session.attachedWs === null) { - sessionManager.detachWs(currentSessionId); + const participant = sessionManager.getParticipantForWs(session, ws); + if (participant && !participant.isOwner) { + sessionManager.removeParticipant(currentSessionId, ws); + } else { + // Only detach if this WS is still the owner's attached socket, or + // no owner is currently attached. If a refresh reconnected and + // reattached a new WS before this close event fired, we must not + // clobber that new attachment. + const ownerStillAttached = Array.from( + session.participants.values(), + ).some((p) => p.isOwner && p.ws !== ws); + if (!ownerStillAttached) { + sessionManager.detachWs(currentSessionId); + } } } else { sessionManager.destroySession(currentSessionId); @@ -295,6 +491,21 @@ wss.on("connection", async (ws: WebSocket, req) => { const { type, data } = parsed; + // Server-side gate: non-owner participants (read-only or read-write + // guests/joiners) may only send input/ping/disconnect - everything else + // (auth flows, tmux, resize, etc.) is owner-only and silently ignored. + if (type !== "joinSharedSession") { + const gateSession = currentSessionId + ? sessionManager.getSession(currentSessionId) + : null; + const gateParticipant = gateSession + ? sessionManager.getParticipantForWs(gateSession, ws) + : null; + if (!isMessageAllowedForParticipant(gateParticipant, type)) { + return; + } + } + switch (type) { case "connectToHost": { const connectData = data as ConnectToHostData; @@ -302,8 +513,7 @@ wss.on("connection", async (ws: WebSocket, req) => { connectData.hostConfig.userId = userId; } handleConnectToHost(connectData).catch((error) => { - const errMsg = - error instanceof Error ? error.message : "Unknown error"; + const errMsg = getErrorMessage(error); if ( errMsg.includes("Cannot parse privateKey") && errMsg.includes("no passphrase") @@ -445,7 +655,20 @@ wss.on("connection", async (ws: WebSocket, req) => { break; } - case "disconnect": + case "disconnect": { + const disconnectSession = currentSessionId + ? sessionManager.getSession(currentSessionId) + : null; + const disconnectParticipant = disconnectSession + ? sessionManager.getParticipantForWs(disconnectSession, ws) + : null; + if (disconnectParticipant && !disconnectParticipant.isOwner) { + if (currentSessionId) { + sessionManager.removeParticipant(currentSessionId, ws); + currentSessionId = null; + } + break; + } if (currentSessionId) { sessionManager.destroySession(currentSessionId); currentSessionId = null; @@ -454,6 +677,7 @@ wss.on("connection", async (ws: WebSocket, req) => { sshConn = null; sshStream = null; break; + } case "get_cwd": { const activeConn = @@ -474,10 +698,8 @@ wss.on("connection", async (ws: WebSocket, req) => { execStream.stderr.on("data", () => {}); execStream.on("close", () => { const cwd = stdout.trim() || "/"; - const attachedWs = - sessionManager.getSession(currentSessionId)?.attachedWs ?? ws; - if (attachedWs.readyState === WebSocket.OPEN) { - attachedWs.send(JSON.stringify({ type: "cwd", path: cwd })); + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "cwd", path: cwd })); } }); }); @@ -517,10 +739,8 @@ wss.on("connection", async (ws: WebSocket, req) => { execStream.stderr.on("data", () => {}); execStream.on("close", () => { const resolvedPath = stdout.trim() || requestedPath; - const attachedWs = - sessionManager.getSession(currentSessionId)?.attachedWs ?? ws; - if (attachedWs.readyState === WebSocket.OPEN) { - attachedWs.send( + if (ws.readyState === WebSocket.OPEN) { + ws.send( JSON.stringify({ type: "open_file_in_editor", path: resolvedPath, @@ -746,8 +966,7 @@ wss.on("connection", async (ws: WebSocket, req) => { }; handleConnectToHost(reconnectData).catch((error) => { - const errMsg = - error instanceof Error ? error.message : "Unknown error"; + const errMsg = getErrorMessage(error); if ( errMsg.includes("Cannot parse privateKey") && errMsg.includes("no passphrase") @@ -887,7 +1106,7 @@ wss.on("connection", async (ws: WebSocket, req) => { type: "error", message: "Failed to connect after authentication: " + - (error instanceof Error ? error.message : "Unknown error"), + getErrorMessage(error), }), ); }); @@ -933,10 +1152,10 @@ wss.on("connection", async (ws: WebSocket, req) => { JSON.stringify({ type: "vault_error", hostId: vaultData.hostId, - error: - error instanceof Error - ? error.message - : "Failed to start Vault authentication", + error: getErrorMessage( + error, + "Failed to start Vault authentication", + ), }), ); } @@ -994,13 +1213,111 @@ wss.on("connection", async (ws: WebSocket, req) => { type: "error", message: "Failed to connect after authentication: " + - (error instanceof Error ? error.message : "Unknown error"), + getErrorMessage(error), }), ); }); break; } + case "joinSharedSession": { + const joinData = data as { shareId: string; tabInstanceId?: string }; + try { + const shareRepo = createCurrentSessionShareRepository(); + const share = await shareRepo.findActiveById(joinData.shareId); + if ( + !share || + share.shareType !== "user" || + share.targetUserId !== userId || + share.protocol !== "ssh" + ) { + ws.send( + JSON.stringify({ + type: "error", + message: "Share not found or not accessible", + }), + ); + break; + } + + const { PermissionManager } = + await import("../../utils/permission-manager.js"); + const access = await PermissionManager.getInstance().canAccessHost( + userId, + share.hostId, + "connect", + ); + if (!access.hasAccess) { + ws.send( + JSON.stringify({ + type: "error", + message: "Share not found or not accessible", + }), + ); + break; + } + + const joinedSession = sessionManager.joinAsParticipant( + share.sessionId, + ws, + { + userId, + permissionLevel: share.permissionLevel as + "read-write" | "read-only", + tabInstanceId: joinData.tabInstanceId, + shareId: share.id, + }, + ); + if (!joinedSession) { + ws.send( + JSON.stringify({ + type: "error", + message: "Shared session is no longer active", + }), + ); + break; + } + + currentSessionId = share.sessionId; + sshStream = joinedSession.sshStream; + sshConn = joinedSession.sshConn; + isConnecting = false; + isConnected = true; + + shareRepo.touchShareUsage(share.id).catch(() => {}); + shareRepo + .recordParticipantJoin(share.id, userId, null) + .catch(() => {}); + + const buffered = sessionManager.getBuffer(joinedSession); + if (buffered) { + ws.send(JSON.stringify({ type: "data", data: buffered })); + } + ws.send( + JSON.stringify({ + type: "sessionAttached", + sessionId: share.sessionId, + }), + ); + ws.send( + JSON.stringify({ type: "connected", message: "Joined session" }), + ); + } catch (error) { + sshLogger.error("Failed to join shared session", error, { + operation: "terminal_join_shared_session_error", + userId, + shareId: joinData.shareId, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "Failed to join shared session", + }), + ); + } + break; + } + default: sshLogger.warn("Unknown message type received", { operation: "websocket_message_unknown_type", @@ -1014,6 +1331,7 @@ wss.on("connection", async (ws: WebSocket, req) => { const { hostConfig, initialPath, executeCommand, tmuxAttachSession } = data; const { id, + syncId: hostSyncId, ip: rawIp, port: clientPort, username: clientUsername, @@ -1110,7 +1428,7 @@ wss.on("connection", async (ws: WebSocket, req) => { sendLog("dns", "info", `Starting address resolution of ${ip}`); sendLog("tcp", "info", `Connecting to ${ip} port ${port}`); - const connectionTimeout = setTimeout(() => { + const onConnectionTimeout = () => { if (sshConn && isConnecting && !isConnected) { sshLogger.error("SSH connection timeout", undefined, { operation: "ssh_connect", @@ -1128,7 +1446,15 @@ wss.on("connection", async (ws: WebSocket, req) => { } cleanupAuthState(connectionTimeout); } - }, 120000); + }; + + // Reassigned when Tailscale check mode starts, so the short connect timeout + // does not tear down a connection the server is deliberately holding open. + let connectionTimeout = setTimeout(onConnectionTimeout, 120000); + + let tailscaleCheckPending = false; + let tailscaleForcePasswordAttempted = false; + let isTailscaleRetrying = false; let resolvedHostData: | (Record & { @@ -1155,11 +1481,65 @@ wss.on("connection", async (ws: WebSocket, req) => { if (id && userId) { try { - const { resolveHostById } = await import("../host-resolver.js"); - resolvedHostData = (await resolveHostById( - id, - userId, - )) as unknown as typeof resolvedHostData; + const { resolveHostById, resolveHostBySyncId } = + await import("../host-resolver.js"); + + // Prefer the sync identity. A numeric id belongs to whichever database + // produced it, so on a sync server it names a different host than the + // desktop app meant; syncId is the same string on both sides. + resolvedHostData = (hostSyncId + ? await resolveHostBySyncId(hostSyncId, userId) + : await resolveHostById(id, userId)) as unknown as + typeof resolvedHostData | null; + + if (hostSyncId && !resolvedHostData) { + sshLogger.error( + "Refusing to connect: host is not known to this server", + undefined, + { + operation: "ssh_connect_host_sync_id_unknown", + hostId: id, + userId, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: HOST_NOT_ON_THIS_SERVER_MESSAGE, + }), + ); + cleanupAuthState(connectionTimeout); + return; + } + + // Older clients send only the numeric id, which cannot be trusted to + // mean the same host here. Everything below is taken from the row it + // lands on -- the address, the credentials, the jump hosts, the stored + // host key -- so compare the address before using any of it. + if ( + !hostSyncId && + hostAddressMismatch(clientIp, resolvedHostData?.ip) + ) { + sshLogger.error( + "Refusing to connect: host id resolves to a different address here", + undefined, + { + operation: "ssh_connect_host_id_mismatch", + hostId: id, + userId, + clientIp, + resolvedIp: resolvedHostData?.ip, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: HOST_ADDRESS_MISMATCH_MESSAGE, + }), + ); + cleanupAuthState(connectionTimeout); + return; + } if (resolvedHostData) { if ( @@ -1206,7 +1586,7 @@ wss.on("connection", async (ws: WebSocket, req) => { sshLogger.warn(`Failed to resolve server-side host data for ${id}`, { operation: "ssh_host_data", hostId: id, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -1247,7 +1627,7 @@ wss.on("connection", async (ws: WebSocket, req) => { sshLogger.warn(`Failed to resolve host credentials for ${id}`, { operation: "ssh_credentials", hostId: id, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } else if (credentialId && id && userId) { @@ -1272,7 +1652,7 @@ wss.on("connection", async (ws: WebSocket, req) => { operation: "ssh_credentials", hostId: id, credentialId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -1288,44 +1668,106 @@ wss.on("connection", async (ws: WebSocket, req) => { }; } - sendLog("dns", "info", `Starting address resolution of ${ip}`); + const connectsViaJumpHosts = !!( + hostConfig.jumpHosts && + hostConfig.jumpHosts.length > 0 && + hostConfig.userId + ); + let connectHost = ip; - try { - const resolution = await resolveHostForSshConnect(ip); - connectHost = resolution.host; - if (resolution.resolvedAddress && resolution.resolvedAddress !== ip) { - sendLog( - "dns", - "success", - `Resolved ${ip} to ${resolution.resolvedAddress}`, - { attempts: resolution.attempts }, - ); - } - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - sshLogger.error("SSH hostname resolution failed", error, { - operation: "terminal_dns_resolve", - hostId: id, - ip, - port, - transient: isRetriableDnsError(error), - }); - sendLog("dns", "error", `DNS resolution failed for ${ip}: ${message}`); - ws.send( - JSON.stringify({ - type: "error", - message: isRetriableDnsError(error) - ? "SSH error: DNS lookup temporarily failed. Check the Docker/container DNS configuration or try again." - : "SSH error: Could not resolve hostname from the Termix server container.", - }), + if (connectsViaJumpHosts) { + // The target is only reachable through the jump host's network (e.g. a + // VPN-only address), so DNS must be resolved there, not on this host. + sendLog( + "dns", + "info", + `Skipping local address resolution of ${ip} (resolved by jump host)`, ); - cleanupAuthState(connectionTimeout); - return; + } else { + sendLog("dns", "info", `Starting address resolution of ${ip}`); + try { + const resolution = await resolveHostForSshConnect(ip); + connectHost = resolution.host; + if (resolution.resolvedAddress && resolution.resolvedAddress !== ip) { + sendLog( + "dns", + "success", + `Resolved ${ip} to ${resolution.resolvedAddress}`, + { attempts: resolution.attempts }, + ); + } + } catch (error) { + const message = getErrorMessage(error); + sshLogger.error("SSH hostname resolution failed", error, { + operation: "terminal_dns_resolve", + hostId: id, + ip, + port, + transient: isRetriableDnsError(error), + }); + sendLog("dns", "error", `DNS resolution failed for ${ip}: ${message}`); + ws.send( + JSON.stringify({ + type: "error", + message: isRetriableDnsError(error) + ? "SSH error: DNS lookup temporarily failed. Check the Docker/container DNS configuration or try again." + : "SSH error: Could not resolve hostname from the Termix server container.", + }), + ); + cleanupAuthState(connectionTimeout); + return; + } } sendLog("tcp", "info", `Connecting to ${ip} port ${port}`); + // Tailscale SSH check mode delivers its re-auth URL as an auth banner and then + // blocks for up to 30 minutes while the user logs in via the browser. + sshConn.on("banner", (banner: string) => { + const check = parseTailscaleCheckBanner(banner); + if (check) { + tailscaleCheckPending = true; + + clearTimeout(connectionTimeout); + connectionTimeout = setTimeout( + onConnectionTimeout, + TAILSCALE_CHECK_TIMEOUT_MS, + ); + + sendLog( + "auth", + "info", + `Tailscale SSH requires an additional check. Waiting for browser authentication at ${check.url}`, + ); + + ws.send( + JSON.stringify({ + type: "tailscale_check_required", + hostId: id, + url: check.url, + message: check.message, + }), + ); + return; + } + + if (tailscaleCheckPending && isTailscaleCheckCompleteBanner(banner)) { + tailscaleCheckPending = false; + sendLog("auth", "info", "Tailscale SSH check completed"); + ws.send( + JSON.stringify({ type: "tailscale_check_completed", hostId: id }), + ); + } + }); + sshConn.on("ready", () => { clearTimeout(connectionTimeout); + isTailscaleRetrying = false; + if (tailscaleCheckPending) { + tailscaleCheckPending = false; + ws.send( + JSON.stringify({ type: "tailscale_check_completed", hostId: id }), + ); + } sshLogger.success("SSH connection established", { operation: "terminal_ssh_connected", sessionId, @@ -1609,22 +2051,27 @@ wss.on("connection", async (ws: WebSocket, req) => { } const boundSessionId = currentSessionId; + // A single TCP/SSH packet boundary can split a multi-byte UTF-8 + // character (e.g. the box-drawing glyphs mc/htop use for borders). + // Buffer.toString("utf-8") on each chunk independently replaces the + // split bytes with U+FFFD, which shows up as corrupted/inserted + // characters. StringDecoder carries incomplete trailing bytes over + // to the next chunk so multi-byte characters decode correctly. + const decoder = new StringDecoder("utf-8"); stream.on("data", (data: Buffer) => { try { - const utf8String = data.toString("utf-8"); + const utf8String = decoder.write(data); if (!utf8String) return; const session = sessionManager.getSession(boundSessionId); if (session) { sessionManager.bufferOutput(boundSessionId!, utf8String); - - if (session.attachedWs?.readyState === WebSocket.OPEN) { - session.attachedWs.send( - JSON.stringify({ type: "data", data: utf8String }), - ); - } + sessionManager.broadcast(boundSessionId!, { + type: "data", + data: utf8String, + }); } } catch (error) { sshLogger.error("Error encoding terminal data", error, { @@ -1636,34 +2083,28 @@ wss.on("connection", async (ws: WebSocket, req) => { const session = sessionManager.getSession(boundSessionId); if (session) { sessionManager.bufferOutput(boundSessionId!, fallback); - - if (session.attachedWs?.readyState === WebSocket.OPEN) { - session.attachedWs.send( - JSON.stringify({ type: "data", data: fallback }), - ); - } + sessionManager.broadcast(boundSessionId!, { + type: "data", + data: fallback, + }); } } }); stream.on("close", (code: number | null) => { const session = sessionManager.getSession(boundSessionId); - if (session?.attachedWs?.readyState === WebSocket.OPEN) { + if (session) { if (code != null) { - session.attachedWs.send( - JSON.stringify({ - type: "session_ended", - code, - }), - ); + sessionManager.broadcast(boundSessionId!, { + type: "session_ended", + code, + }); } else { - session.attachedWs.send( - JSON.stringify({ - type: "disconnected", - message: "Connection lost", - graceful: true, - }), - ); + sessionManager.broadcast(boundSessionId!, { + type: "disconnected", + message: "Connection lost", + graceful: true, + }); } } if (boundSessionId) { @@ -1683,13 +2124,11 @@ wss.on("connection", async (ws: WebSocket, req) => { username, }); const session = sessionManager.getSession(boundSessionId); - if (session?.attachedWs?.readyState === WebSocket.OPEN) { - session.attachedWs.send( - JSON.stringify({ - type: "error", - message: "SSH stream error: " + err.message, - }), - ); + if (session) { + sessionManager.broadcast(boundSessionId!, { + type: "error", + message: "SSH stream error: " + err.message, + }); } }); @@ -1815,7 +2254,7 @@ wss.on("connection", async (ws: WebSocket, req) => { id, hostConfig.userId, username, - req.socket.remoteAddress ?? "unknown", + getClientIp(req), ).catch(() => {}); } @@ -1851,8 +2290,7 @@ wss.on("connection", async (ws: WebSocket, req) => { operation: "activity_log_error", userId: hostConfig.userId, hostId: id, - error: - error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } })(); @@ -2006,6 +2444,51 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } + // Tailscale documents the "+password" username suffix as the workaround for + // clients that mishandle a successful reply to auth type "none". It routes + // through PasswordCallback into the same check-mode flow, and the password + // value is ignored. Retry once before reporting an auth failure. + // Skipped when tunnelled: connectConfig.sock is a one-shot stream that + // cannot be reused for a second connect. + if ( + resolvedCredentials.authType === "tailscale" && + !tailscaleForcePasswordAttempted && + !tailscaleCheckPending && + !connectConfig.sock && + (authMethodNotAvailable || + err.message.includes("All configured authentication methods failed")) + ) { + tailscaleForcePasswordAttempted = true; + + sendLog( + "auth", + "info", + "Retrying Tailscale SSH in forced password mode", + ); + sshLogger.info("Retrying Tailscale SSH with +password suffix", { + operation: "tailscale_force_password_retry", + hostId: id, + userId, + username, + }); + + clearTimeout(connectionTimeout); + connectionTimeout = setTimeout( + onConnectionTimeout, + TAILSCALE_CHECK_TIMEOUT_MS, + ); + + connectConfig.username = `${username}+password`; + connectConfig.password = "termix"; + connectConfig.tryKeyboard = false; + + // ssh2's connect() ends an open socket and reconnects on close, keeping + // every listener attached, so the same client can be reused here. + isTailscaleRetrying = true; + sshConn.connect(connectConfig); + return; + } + if ( resolvedCredentials.authType === "tailscale" && (authMethodNotAvailable || @@ -2014,7 +2497,7 @@ wss.on("connection", async (ws: WebSocket, req) => { sendLog( "auth", "error", - "Tailscale SSH authentication failed. Ensure Tailscale is running on the server, SSH is advertised (tailscale set --ssh), and your ACL policy permits this connection.", + `Tailscale SSH authentication failed for user "${username}". Ensure Tailscale is running on the server, SSH is advertised (tailscale set --ssh), and your ACL policy grants the "${username}" user to your identity (check tailscale.com/s/ssh for the check/action ACL syntax). If your Tailscale identity maps to a different Unix user, update the username on this host.`, ); if (currentSessionId) { sessionManager.destroySession(currentSessionId); @@ -2024,8 +2507,7 @@ wss.on("connection", async (ws: WebSocket, req) => { ws.send( JSON.stringify({ type: "error", - message: - "Tailscale SSH authentication failed. Ensure Tailscale is running on the server, SSH is advertised (tailscale set --ssh), and your ACL policy permits this connection.", + message: `Tailscale SSH authentication failed for user "${username}". Ensure Tailscale is running on the server, SSH is advertised (tailscale set --ssh), and your ACL policy grants the "${username}" user to your identity. If your Tailscale identity maps to a different Unix user, update the username on this host.`, }), ); return; @@ -2168,6 +2650,12 @@ wss.on("connection", async (ws: WebSocket, req) => { }); sshConn.on("close", () => { + // The +password retry ends the socket before reconnecting; that close is + // part of the retry, not a disconnect. + if (isTailscaleRetrying) { + return; + } + clearTimeout(connectionTimeout); sshLogger.info("SSH connection closed", { operation: "terminal_ssh_disconnected", @@ -2279,6 +2767,12 @@ wss.on("connection", async (ws: WebSocket, req) => { const hostKeepaliveInterval = hostConfig.terminalConfig?.keepaliveInterval; const hostKeepaliveCountMax = hostConfig.terminalConfig?.keepaliveCountMax; + const keepalive = resolveSshKeepalive( + hostKeepaliveInterval, + hostKeepaliveCountMax, + 30000, + 5, + ); // Pre-fetch the stored host key before connect so the verifier callback // runs synchronously during SSH key exchange, avoiding LoginGraceTime @@ -2290,18 +2784,18 @@ wss.on("connection", async (ws: WebSocket, req) => { port, username, tryKeyboard: resolvedCredentials.authType !== "tailscale", - keepaliveInterval: - typeof hostKeepaliveInterval === "number" - ? Math.max(5000, hostKeepaliveInterval * 1000) - : 30000, - keepaliveCountMax: - typeof hostKeepaliveCountMax === "number" - ? Math.max(1, hostKeepaliveCountMax) - : 5, - readyTimeout: 120000, + ...keepalive, + readyTimeout: + resolvedCredentials.authType === "tailscale" + ? TAILSCALE_CHECK_TIMEOUT_MS + : 120000, tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, - timeout: 120000, + // The socket sits idle while a Tailscale check-mode login is pending. + timeout: + resolvedCredentials.authType === "tailscale" + ? TAILSCALE_CHECK_TIMEOUT_MS + : 120000, hostVerifier: await SSHHostKeyVerifier.createHostVerifier( id, ip, @@ -2402,18 +2896,12 @@ wss.on("connection", async (ws: WebSocket, req) => { operation: "ca_cert_auth_setup_failed", userId, hostId: id, - error: - certError instanceof Error - ? certError.message - : String(certError), + error: getErrorMessage(certError, String(certError)), }); } } } catch (keyError) { - const message = - keyError instanceof Error - ? keyError.message - : "Invalid private key format"; + const message = getErrorMessage(keyError, "Invalid private key format"); sshLogger.error("SSH key format error: " + message); ws.send( JSON.stringify({ @@ -2472,10 +2960,7 @@ wss.on("connection", async (ws: WebSocket, req) => { JSON.stringify({ type: "error", message: - "OPKSSH authentication failed: " + - (opksshError instanceof Error - ? opksshError.message - : "Unknown error"), + "OPKSSH authentication failed: " + getErrorMessage(opksshError), }), ); return; @@ -2484,8 +2969,7 @@ wss.on("connection", async (ws: WebSocket, req) => { sendLog("auth", "info", "Using Vault SSH signer authentication"); try { const vaultProfile = resolvedHostData?.vaultProfile as - | { id: number } - | undefined; + { id: number } | undefined; if (!vaultProfile?.id) { throw new Error("Host has no Vault signer profile configured"); } @@ -2528,9 +3012,7 @@ wss.on("connection", async (ws: WebSocket, req) => { type: "error", message: "Vault SSH signer authentication failed: " + - (vaultError instanceof Error - ? vaultError.message - : "Unknown error"), + getErrorMessage(vaultError), }), ); return; @@ -2634,8 +3116,7 @@ wss.on("connection", async (ws: WebSocket, req) => { // Cloudflare Tunnel: connect via WebSocket proxy const cfConfig = hostConfig.terminalConfig as - | Record - | undefined; + Record | undefined; if (cfConfig?.cfAccessClientId && cfConfig?.cfAccessClientSecret) { try { const WebSocket = (await import("ws")).default; @@ -2680,7 +3161,7 @@ wss.on("connection", async (ws: WebSocket, req) => { type: "error", message: "Cloudflare tunnel connection failed: " + - (cfError instanceof Error ? cfError.message : "Unknown error"), + getErrorMessage(cfError), }), ); cleanupAuthState(connectionTimeout); @@ -2693,7 +3174,6 @@ wss.on("connection", async (ws: WebSocket, req) => { const jumpClient = await createJumpHostChain( hostConfig.jumpHosts!, hostConfig.userId!, - proxyConfig, ); if (!jumpClient) { @@ -2791,11 +3271,7 @@ wss.on("connection", async (ws: WebSocket, req) => { ws.send( JSON.stringify({ type: "error", - message: - "Proxy connection failed: " + - (proxyError instanceof Error - ? proxyError.message - : "Unknown error"), + message: "Proxy connection failed: " + getErrorMessage(proxyError), }), ); if (currentSessionId) { diff --git a/src/backend/hosts/terminal/session-manager.ts b/src/backend/hosts/terminal/session-manager.ts index 1e1d384..98c583b 100644 --- a/src/backend/hosts/terminal/session-manager.ts +++ b/src/backend/hosts/terminal/session-manager.ts @@ -14,6 +14,22 @@ const SESSION_LOGS_DIR = path.join(DATA_DIR, "session_logs"); const DEFAULT_TIMEOUT_MINUTES = 30; const HEALTH_CHECK_INTERVAL_MS = 60_000; const MAX_SESSIONS_PER_USER = 10; +// Coalesces recording writes: a chatty SSH stream can emit dozens of "data" +// events per second, and appending to disk on every single one saturates the +// libuv threadpool (default size 4), starving unrelated fs/DNS/crypto work +// and stalling the WS ping/pong health check enough to look like connection +// drops. Batch pending lines and flush on a short trailing edge instead. +const RECORDING_FLUSH_INTERVAL_MS = 300; + +export interface SessionParticipant { + ws: WebSocket; + userId: string | null; // null for anonymous link guests + permissionLevel: "read-write" | "read-only"; + isOwner: boolean; + guestLabel?: string; + tabInstanceId?: string; + joinedViaShareId?: string; +} export interface TerminalSession { id: string; @@ -32,7 +48,7 @@ export interface TerminalSession { isConnected: boolean; createdAt: number; - attachedWs: WebSocket | null; + participants: Map; lastDetachedAt: number | null; detachTimeout: NodeJS.Timeout | null; @@ -44,10 +60,39 @@ export interface TerminalSession { recordingId: number | null; recordingWriteChain: Promise; recordingPersistChain: Promise; + pendingRecordingData: string; + recordingFlushTimer: NodeJS.Timeout | null; tmuxSessionName: string | null; sessionLoggingEnabled: boolean; sessionStartedAt: number; lastPersistedBytes: number; + terminatedByOwner: boolean; + terminationReason: string | null; +} + +/** Message types a non-owner participant may legally send. */ +const NON_OWNER_ALLOWED_MESSAGE_TYPES = new Set([ + "input", + "ping", + "disconnect", +]); + +/** + * Server-side gate for whether a participant may send a given WS message + * type. The owner may send anything; non-owners are limited to input (if + * read-write), ping, and disconnect. Pure function so read-only enforcement + * is unit-testable without a real WebSocketServer. + */ +export function isMessageAllowedForParticipant( + participant: Pick | null, + messageType: string, +): boolean { + if (!participant || participant.isOwner) return true; + if (!NON_OWNER_ALLOWED_MESSAGE_TYPES.has(messageType)) return false; + if (messageType === "input" && participant.permissionLevel === "read-only") { + return false; + } + return true; } class TerminalSessionManager { @@ -81,7 +126,7 @@ class TerminalSessionManager { const userSessions = this.getUserSessions(userId); if (userSessions.length >= MAX_SESSIONS_PER_USER) { const detached = userSessions - .filter((s) => s.attachedWs === null) + .filter((s) => this.getOwnerParticipant(s) === null) .sort( (a, b) => (a.lastDetachedAt ?? a.createdAt) - @@ -109,7 +154,7 @@ class TerminalSessionManager { operation: "session_tab_duplicate_skip", existingSessionId: existing.id, tabInstanceId, - hasAttachedWs: existing.attachedWs !== null, + hasAttachedWs: this.getOwnerParticipant(existing) !== null, }, ); return existing.id; @@ -151,7 +196,7 @@ class TerminalSessionManager { rows, isConnected: false, createdAt: now, - attachedWs: null, + participants: new Map(), lastDetachedAt: null, detachTimeout: null, outputBuffer: [], @@ -162,10 +207,14 @@ class TerminalSessionManager { recordingId: null, recordingWriteChain: Promise.resolve(), recordingPersistChain: Promise.resolve(), + pendingRecordingData: "", + recordingFlushTimer: null, tmuxSessionName: null, sessionLoggingEnabled, sessionStartedAt: now, lastPersistedBytes: 0, + terminatedByOwner: false, + terminationReason: null, }; this.sessions.set(id, session); @@ -199,6 +248,25 @@ class TerminalSessionManager { session.isConnected = true; } + /** Finds the owner's participant entry, if currently attached. */ + private getOwnerParticipant( + session: TerminalSession, + ): SessionParticipant | null { + for (const participant of session.participants.values()) { + if (participant.isOwner) return participant; + } + return null; + } + + private getOwnerEntry( + session: TerminalSession, + ): [string, SessionParticipant] | null { + for (const entry of session.participants.entries()) { + if (entry[1].isOwner) return entry; + } + return null; + } + attachWs( sessionId: string, userId: string, @@ -234,8 +302,9 @@ class TerminalSessionManager { return null; } + const ownerParticipant = this.getOwnerParticipant(session); const isDetached = - !session.attachedWs || session.attachedWs.readyState !== WebSocket.OPEN; + !ownerParticipant || ownerParticipant.ws.readyState !== WebSocket.OPEN; const isOriginalTab = (session.attachedTabInstanceId ?? session.tabInstanceId) === tabInstanceId; @@ -282,9 +351,10 @@ class TerminalSessionManager { ); } - if (session.attachedWs && session.attachedWs !== ws) { + const ownerEntry = this.getOwnerEntry(session); + if (ownerEntry && ownerEntry[1].ws !== ws) { try { - session.attachedWs.send( + ownerEntry[1].ws.send( JSON.stringify({ type: "sessionTakenOver", sessionId, @@ -294,7 +364,7 @@ class TerminalSessionManager { } catch { /* ignore */ } - session.attachedWs = null; + session.participants.delete(ownerEntry[0]); } if (session.detachTimeout) { @@ -302,7 +372,14 @@ class TerminalSessionManager { session.detachTimeout = null; } - session.attachedWs = ws; + const participantId = crypto.randomUUID(); + session.participants.set(participantId, { + ws, + userId, + permissionLevel: "read-write", + isOwner: true, + tabInstanceId, + }); session.attachedTabInstanceId = tabInstanceId; session.lastDetachedAt = null; @@ -316,6 +393,110 @@ class TerminalSessionManager { return session; } + /** + * Adds a non-owner participant (in-app share join or anonymous link guest). + * Purely additive - never evicts the owner or any other participant. + */ + joinAsParticipant( + sessionId: string, + ws: WebSocket, + opts: { + userId: string | null; + permissionLevel: "read-write" | "read-only"; + guestLabel?: string; + tabInstanceId?: string; + shareId?: string; + }, + ): TerminalSession | null { + const session = this.sessions.get(sessionId); + if (!session || !session.isConnected) return null; + + const participantId = crypto.randomUUID(); + session.participants.set(participantId, { + ws, + userId: opts.userId, + permissionLevel: opts.permissionLevel, + isOwner: false, + guestLabel: opts.guestLabel, + tabInstanceId: opts.tabInstanceId, + joinedViaShareId: opts.shareId, + }); + + sshLogger.info("Participant joined shared session", { + operation: "session_join_participant", + sessionId, + userId: opts.userId, + permissionLevel: opts.permissionLevel, + shareId: opts.shareId, + }); + + return session; + } + + /** Fans out a message to every OPEN participant socket; skips closed ones and send failures. */ + broadcast(sessionId: string, message: object): void { + const session = this.sessions.get(sessionId); + if (!session) return; + const payload = JSON.stringify(message); + for (const participant of session.participants.values()) { + if (participant.ws.readyState !== WebSocket.OPEN) continue; + try { + participant.ws.send(payload); + } catch { + /* ignore individual send failures, keep broadcasting to the rest */ + } + } + } + + /** Finds the participant entry (owner or not) for a given socket. */ + getParticipantForWs( + session: TerminalSession, + ws: WebSocket, + ): SessionParticipant | null { + for (const participant of session.participants.values()) { + if (participant.ws === ws) return participant; + } + return null; + } + + /** + * Removes a non-owner participant's socket. No detach timeout or session + * destruction side effects - a guest leaving must never end the session. + */ + removeParticipant(sessionId: string, ws: WebSocket): void { + const session = this.sessions.get(sessionId); + if (!session) return; + for (const [id, participant] of session.participants.entries()) { + if (participant.ws === ws && !participant.isOwner) { + session.participants.delete(id); + sshLogger.info("Participant left shared session", { + operation: "session_leave_participant", + sessionId, + userId: participant.userId, + }); + return; + } + } + } + + /** Broadcasts termination to all guests, then destroys the session. */ + ownerEndSession(sessionId: string, reason: string): void { + const session = this.sessions.get(sessionId); + if (!session) return; + + this.broadcast(sessionId, { type: "sessionTerminatedByOwner", reason }); + session.terminatedByOwner = true; + session.terminationReason = reason; + + sshLogger.info("Owner ended shared session", { + operation: "session_owner_end", + sessionId, + reason, + }); + + this.destroySession(sessionId); + } + detachWs(sessionId: string): void { const session = this.sessions.get(sessionId); if (!session) return; @@ -325,7 +506,10 @@ class TerminalSessionManager { session.detachTimeout = null; } - session.attachedWs = null; + const ownerEntry = this.getOwnerEntry(session); + if (ownerEntry) { + session.participants.delete(ownerEntry[0]); + } session.lastDetachedAt = Date.now(); // Persist log immediately when the user detaches so it appears right away, @@ -365,6 +549,23 @@ class TerminalSessionManager { fs.promises.unlink(session.recordingPath).catch(() => {}); } + for (const participant of session.participants.values()) { + if (participant.isOwner) continue; + if (participant.ws.readyState !== WebSocket.OPEN) continue; + try { + participant.ws.send( + JSON.stringify({ + type: "sessionExpired", + sessionId, + message: "Session has ended", + }), + ); + } catch { + /* ignore */ + } + } + session.participants.clear(); + if (session.sshStream) { try { session.sshStream.end(); @@ -408,6 +609,11 @@ class TerminalSessionManager { private maybePersistLog(session: TerminalSession, force = false): void { if (!session.sessionLoggingEnabled) return; + if (session.recordingFlushTimer) { + clearTimeout(session.recordingFlushTimer); + session.recordingFlushTimer = null; + this.flushRecording(session); + } if (session.recordingBytes === 0) return; if (!force && session.recordingBytes === session.lastPersistedBytes) return; session.lastPersistedBytes = session.recordingBytes; @@ -440,12 +646,16 @@ class TerminalSessionManager { recordingPath: session.recordingPath, protocol: "ssh", format: "asciicast", + terminatedByOwner: session.terminatedByOwner || undefined, + terminationReason: session.terminationReason ?? undefined, }); session.recordingId = created.id; } else { await repo.updateEnded(session.recordingId, { endedAt: new Date(endedAt).toISOString(), duration, + terminatedByOwner: session.terminatedByOwner || undefined, + terminationReason: session.terminationReason ?? undefined, }); } } catch (err) { @@ -515,21 +725,37 @@ class TerminalSessionManager { return; const elapsed = (Date.now() - session.sessionStartedAt) / 1000; const line = `${JSON.stringify([elapsed, type, data])}\n`; - const firstEvent = session.recordingBytes === 0; session.recordingBytes += Buffer.byteLength(line); + session.pendingRecordingData += line; + + if (!session.recordingFlushTimer) { + session.recordingFlushTimer = setTimeout(() => { + session.recordingFlushTimer = null; + this.flushRecording(session); + }, RECORDING_FLUSH_INTERVAL_MS); + } + } + + /** Coalesces buffered recording lines into a single disk write. */ + private flushRecording(session: TerminalSession): void { + if (!session.recordingPath || !session.pendingRecordingData) return; + const chunk = session.pendingRecordingData; + session.pendingRecordingData = ""; + const firstWrite = session.recordingBytes === Buffer.byteLength(chunk); + session.recordingWriteChain = session.recordingWriteChain.then(async () => { - if (firstEvent) { + if (firstWrite) { await fs.promises.mkdir(path.dirname(session.recordingPath!), { recursive: true, }); await fs.promises.writeFile( session.recordingPath!, - `${session.recordingHeader}${line}`, + `${session.recordingHeader}${chunk}`, "utf8", ); return; } - await fs.promises.appendFile(session.recordingPath!, line, "utf8"); + await fs.promises.appendFile(session.recordingPath!, chunk, "utf8"); }); } @@ -569,10 +795,10 @@ class TerminalSessionManager { for (const [id, session] of this.sessions) { if (!session.isConnected) continue; - if ( - session.attachedWs && - session.attachedWs.readyState === WebSocket.OPEN - ) { + const hasOpenParticipant = Array.from(session.participants.values()).some( + (p) => p.ws.readyState === WebSocket.OPEN, + ); + if (hasOpenParticipant) { continue; } diff --git a/src/backend/hosts/tmux/auth-utils.ts b/src/backend/hosts/tmux/auth-utils.ts new file mode 100644 index 0000000..726b1f8 --- /dev/null +++ b/src/backend/hosts/tmux/auth-utils.ts @@ -0,0 +1,11 @@ +import type { SSHHost } from "../../../types/index.js"; + +export function getTmuxAuthBehavior(authType: SSHHost["authType"]): { + credentialless: boolean; + tryKeyboard: boolean; +} { + return { + credentialless: authType === "none" || authType === "tailscale", + tryKeyboard: authType !== "tailscale", + }; +} diff --git a/src/backend/hosts/tmux/helper.ts b/src/backend/hosts/tmux/helper.ts index 22b886c..2c5fc3a 100644 --- a/src/backend/hosts/tmux/helper.ts +++ b/src/backend/hosts/tmux/helper.ts @@ -22,12 +22,12 @@ const TMUX_PATH_DIRS = [ ]; export function withTmuxPath(command: string): string { - const script = `PATH=${TMUX_PATH_DIRS.join(":")}:$PATH; export PATH; ${command}`; + const script = `PATH=${TMUX_PATH_DIRS.join(":")}:"$PATH"; export PATH; ${command}`; return `/bin/sh -c ${shellEscape(script)}`; } export function tmuxCommand(args: string): string { - return withTmuxPath(`tmux ${args}`); + return withTmuxPath(`tmux -u ${args}`); } /** @@ -184,21 +184,6 @@ export function attachOrCreateTmuxSession( /** * Query the name of the most recently created tmux session via exec channel. */ -export async function queryNewestTmuxSession( - conn: Client, -): Promise { - try { - const output = await execCommand( - conn, - tmuxCommand( - `list-sessions -F "#{session_created}:#{session_name}" 2>/dev/null | sort -rn | head -1 | cut -d: -f2-`, - ), - ); - return output || null; - } catch { - return null; - } -} function shellEscape(s: string): string { return "'" + s.replace(/'/g, "'\\''") + "'"; diff --git a/src/backend/hosts/tmux/index.ts b/src/backend/hosts/tmux/index.ts index 1755c5c..ba0901c 100644 --- a/src/backend/hosts/tmux/index.ts +++ b/src/backend/hosts/tmux/index.ts @@ -1,7 +1,9 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import express from "express"; import cookieParser from "cookie-parser"; import { Client, type ConnectConfig } from "ssh2"; import { createCorsMiddleware } from "../../utils/cors-config.js"; +import { createCompressionMiddleware } from "../../utils/compression-config.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { @@ -40,6 +42,7 @@ import { type PaneMetrics, } from "./monitor-helpers.js"; import type { SSHHost, AuthenticatedRequest } from "../../../types/index.js"; +import { getTmuxAuthBehavior } from "./auth-utils.js"; const PANE_ID_RE = /^%\d+$/; // tmux session names cannot contain ":" or "."; keep to a conservative @@ -59,11 +62,12 @@ interface TmuxSessionOverview extends TmuxSessionSummary { // and docker; jump hosts and SOCKS5 reuse the shared helpers) async function buildSshConfig(host: SSHHost): Promise { + const authBehavior = getTmuxAuthBehavior(host.authType); const base: ConnectConfig = { host: (host.ip || "").replace(/^\[|\]$/g, ""), port: host.port, username: host.username, - tryKeyboard: true, + tryKeyboard: authBehavior.tryKeyboard, keepaliveInterval: 30000, keepaliveCountMax: 3, readyTimeout: 60000, @@ -94,7 +98,7 @@ async function buildSshConfig(host: SSHHost): Promise { if (host.keyPassword) { (base as Record).passphrase = host.keyPassword; } - } else if (host.authType === "none") { + } else if (authBehavior.credentialless) { // no credentials needed } else if (host.authType === "vault") { // cert auth setup happens in connectToHost (needs client instance) @@ -143,11 +147,7 @@ export function connectToHost(host: SSHHost): () => Promise { let jumpClient: Client | null = null; if (host.jumpHosts && host.jumpHosts.length > 0 && host.userId) { - jumpClient = await createJumpHostChain( - host.jumpHosts, - host.userId, - proxyConfig, - ); + jumpClient = await createJumpHostChain(host.jumpHosts, host.userId); if (!jumpClient) { throw new Error("Failed to establish jump host chain"); } @@ -328,6 +328,7 @@ async function collectPaneMetrics( const app = express(); const authManager = AuthManager.getInstance(); +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"])); app.use(cookieParser()); app.use(express.json({ limit: "1mb" })); @@ -391,7 +392,7 @@ async function requireHost( } function toErrorMessage(err: unknown): string { - return err instanceof Error ? err.message : "Unknown error"; + return getErrorMessage(err); } // Destructive tmux actions terminate processes on the remote host, so they @@ -429,13 +430,10 @@ async function auditTmuxAction( // Typed error codes so the frontend can render a helpful state instead of a // raw 500 (same pattern as SESSION_EXPIRED handling in main-axios). type TmuxErrorCode = - | "TMUX_NOT_INSTALLED" - | "TMUX_NO_SERVER" - | "HOST_UNREACHABLE" - | "TMUX_ERROR"; + "TMUX_NOT_INSTALLED" | "TMUX_NO_SERVER" | "HOST_UNREACHABLE" | "TMUX_ERROR"; function classifyTmuxError(err: unknown): TmuxErrorCode { - const msg = err instanceof Error ? err.message : ""; + const msg = getErrorMessage(err, ""); if (/command not found|exited with code 127/i.test(msg)) return "TMUX_NOT_INSTALLED"; if (/no server running|lost server/i.test(msg)) return "TMUX_NO_SERVER"; diff --git a/src/backend/hosts/tunnel/index.ts b/src/backend/hosts/tunnel/index.ts index bbbfc4f..b760fb9 100644 --- a/src/backend/hosts/tunnel/index.ts +++ b/src/backend/hosts/tunnel/index.ts @@ -2,6 +2,7 @@ import express from "express"; import { createServer } from "http"; import { createCorsMiddleware } from "../../utils/cors-config.js"; +import { createCompressionMiddleware } from "../../utils/compression-config.js"; import cookieParser from "cookie-parser"; import { WebSocketServer } from "ws"; @@ -25,6 +26,7 @@ import { initializeAutoStartTunnels } from "./manager.js"; const authManager = AuthManager.getInstance(); const app = express(); +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"])); app.use(cookieParser()); app.use(express.json()); @@ -42,9 +44,21 @@ const c2sRelayWss = new WebSocketServer({ path: "/ssh/tunnel/c2s/stream", }); +c2sRelayWss.on("error", (error) => { + tunnelLogger.error("C2S relay WebSocket server error", error, { + operation: "c2s_relay_wss_error", + }); +}); + c2sRelayWss.on("connection", (ws, req) => { let opened = false; + ws.on("error", (error) => { + tunnelLogger.error("C2S relay WebSocket connection error", error, { + operation: "c2s_relay_ws_error", + }); + }); + ws.once("message", async (raw) => { try { const token = extractRequestToken(req); diff --git a/src/backend/hosts/tunnel/manager.ts b/src/backend/hosts/tunnel/manager.ts index 527c593..59a36d3 100644 --- a/src/backend/hosts/tunnel/manager.ts +++ b/src/backend/hosts/tunnel/manager.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import { type Response } from "express"; import { createServer as createTcpServer, @@ -42,7 +43,9 @@ import { } from "./utils.js"; import { resolveSshConnectConfigHost } from "../ssh-dns.js"; +import { PermissionManager } from "../../utils/permission-manager.js"; import { handleSocks5Connect } from "./socks5-relay.js"; +import { notifyAutomationInternalEvent } from "../metrics/automation-bridge.js"; export const activeTunnels = new Map(); export const retryCounters = new Map(); @@ -64,7 +67,9 @@ export const lastTunnelErrorTypes = new Map< export const tunnelConfigs = new Map(); export const activeTunnelProcesses = new Map(); export const pendingTunnelOperations = new Map>(); -export const tunnelStatusClients = new Set(); +// SSE clients mapped to the user they belong to, so snapshots can be +// filtered to tunnels that user can actually reach. +export const tunnelStatusClients = new Map(); export const INTERNAL_HOST_API_BASE_URL = "http://localhost:30001/host/db/host"; export const AUTOSTART_FETCH_RETRIES = 6; @@ -80,7 +85,7 @@ export function describeAxiosError(error: unknown): string { : error.message; } - return error instanceof Error ? error.message : "Unknown error"; + return getErrorMessage(error); } export async function fetchInternalHosts( @@ -213,18 +218,63 @@ export function getAllTunnelStatus(): Record { return tunnelStatus; } +// Tunnel names embed host labels and destination endpoints, and error text +// can leak internal hostnames -- so visibility is scoped to tunnels whose +// source host the requesting user can access (owner, share grant, admin), +// mirroring the ownership model of the connect/disconnect routes. +export async function getTunnelStatusForUser( + userId: string, +): Promise> { + const permissionManager = PermissionManager.getInstance(); + const accessibleHostIds = await permissionManager.filterAccessibleHostIds( + userId, + [...tunnelConfigs.values()] + .map((config) => config.sourceHostId) + .filter((id) => Number.isInteger(id)), + ); + + const tunnelStatus: Record = {}; + connectionStatus.forEach((status, name) => { + const sourceHostId = tunnelConfigs.get(name)?.sourceHostId; + if (sourceHostId !== undefined && accessibleHostIds.has(sourceHostId)) { + tunnelStatus[name] = status; + } + }); + return tunnelStatus; +} + +export async function canAccessTunnel( + userId: string, + tunnelName: string, +): Promise { + const sourceHostId = tunnelConfigs.get(tunnelName)?.sourceHostId; + if (sourceHostId === undefined) return false; + const permissionManager = PermissionManager.getInstance(); + const access = await permissionManager.canAccessHost( + userId, + sourceHostId, + "connect", + ); + return access.hasAccess; +} + export function sendTunnelStatusSnapshot(res: Response): void { - try { - res.write( - `event: statuses\ndata: ${JSON.stringify(getAllTunnelStatus())}\n\n`, - ); - } catch { + const userId = tunnelStatusClients.get(res); + if (userId === undefined) { tunnelStatusClients.delete(res); + return; } + void getTunnelStatusForUser(userId) + .then((statuses) => { + res.write(`event: statuses\ndata: ${JSON.stringify(statuses)}\n\n`); + }) + .catch(() => { + tunnelStatusClients.delete(res); + }); } export function broadcastTunnelStatusSnapshot(): void { - for (const client of tunnelStatusClients) { + for (const client of tunnelStatusClients.keys()) { sendTunnelStatusSnapshot(client); } } @@ -405,6 +455,18 @@ export async function handleDisconnect( return; } + // Past the manual branch, so this is a drop the user did not ask for. + const ownerUserId = + tunnelConfig?.sourceUserId ?? tunnelConfig?.requestingUserId; + if (ownerUserId) { + notifyAutomationInternalEvent( + "tunnel_disconnected", + ownerUserId, + tunnelConfig?.sourceHostId, + { tunnelName }, + ); + } + if (retryExhaustedTunnels.has(tunnelName)) { broadcastTunnelStatus(tunnelName, { connected: false, @@ -492,7 +554,7 @@ export async function handleDisconnect( activeTunnels.delete(tunnelName); connectSSHTunnel(tunnelConfig, retryCount).catch((error) => { tunnelLogger.error( - `Failed to connect tunnel ${tunnelConfig.name}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to connect tunnel ${tunnelConfig.name}: ${getErrorMessage(error)}`, ); }); } @@ -966,7 +1028,7 @@ export async function connectSSHTunnel( operation: "tunnel_connect", tunnelName, sourceHostId: tunnelConfig.sourceHostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } else if (tunnelConfig.sourceCredentialId && effectiveUserId) { @@ -993,7 +1055,7 @@ export async function connectSSHTunnel( operation: "tunnel_connect", tunnelName, credentialId: tunnelConfig.sourceCredentialId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -1019,8 +1081,7 @@ export async function connectSSHTunnel( resolvedEndpointCredentials = { password: credential.password as string | undefined, sshKey: (credential.key || credential.privateKey) as - | string - | undefined, + string | undefined, keyPassword: credential.keyPassword as string | undefined, keyType: credential.keyType as string | undefined, authMethod: credential.authType as string, @@ -1035,7 +1096,7 @@ export async function connectSSHTunnel( } } catch (error) { tunnelLogger.warn( - `Failed to resolve endpoint credentials for tunnel ${tunnelName}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to resolve endpoint credentials for tunnel ${tunnelName}: ${getErrorMessage(error)}`, ); } } else if (tunnelConfig.endpointCredentialId) { @@ -1264,8 +1325,7 @@ export async function connectSSHTunnel( }); setupPingInterval(tunnelName); } catch (error) { - const message = - error instanceof Error ? error.message : "Failed to create tunnel"; + const message = getErrorMessage(error, "Failed to create tunnel"); const errorType = classifyTunnelError(message); tunnelLogger.error("Failed to create managed tunnel", error, { operation: "managed_tunnel_create_failed", @@ -1363,8 +1423,7 @@ export async function connectSSHTunnel( resolvedSourceCredentials.keyPassword, ); } catch (error) { - const message = - error instanceof Error ? error.message : "Invalid SSH key format"; + const message = getErrorMessage(error, "Invalid SSH key format"); tunnelLogger.error( `Invalid SSH key format for tunnel '${tunnelName}': ${message}`, undefined, @@ -1459,17 +1518,13 @@ export async function connectSSHTunnel( hasProxyAuth: !!( tunnelConfig.socks5Username && tunnelConfig.socks5Password ), - errorMessage: - socks5Error instanceof Error ? socks5Error.message : "Unknown error", + errorMessage: getErrorMessage(socks5Error), }); broadcastTunnelStatus(tunnelName, { connected: false, status: CONNECTION_STATES.FAILED, reason: - "SOCKS5 proxy connection failed: " + - (socks5Error instanceof Error - ? socks5Error.message - : "Unknown error"), + "SOCKS5 proxy connection failed: " + getErrorMessage(socks5Error), }); tunnelConnecting.delete(tunnelName); return; @@ -1488,10 +1543,10 @@ export async function connectSSHTunnel( broadcastTunnelStatus(tunnelName, { connected: false, status: CONNECTION_STATES.FAILED, - reason: - error instanceof Error - ? error.message - : "Failed to resolve tunnel source hostname", + reason: getErrorMessage( + error, + "Failed to resolve tunnel source hostname", + ), }); tunnelConnecting.delete(tunnelName); return; @@ -1547,7 +1602,7 @@ export async function killRemoteTunnelByMarker( tunnelLogger.warn("Failed to resolve source credentials for cleanup", { tunnelName, sourceHostId: tunnelConfig.sourceHostId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -1674,10 +1729,7 @@ export async function killRemoteTunnelByMarker( }, ); throw new Error( - "SOCKS5 proxy connection failed: " + - (socks5Error instanceof Error - ? socks5Error.message - : "Unknown error"), + "SOCKS5 proxy connection failed: " + getErrorMessage(socks5Error), { cause: socks5Error }, ); } @@ -1892,7 +1944,7 @@ export async function initializeAutoStartTunnels(): Promise { setTimeout(() => { connectSSHTunnel(tunnelConfig, 0).catch((error) => { tunnelLogger.error( - `Failed to connect tunnel ${tunnelConfig.name}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to connect tunnel ${tunnelConfig.name}: ${getErrorMessage(error)}`, ); }); }, 1000); @@ -1900,7 +1952,7 @@ export async function initializeAutoStartTunnels(): Promise { } catch (error) { tunnelLogger.error( "Failed to initialize auto-start tunnels:", - error instanceof Error ? error.message : "Unknown error", + getErrorMessage(error), ); } } diff --git a/src/backend/hosts/tunnel/routes.ts b/src/backend/hosts/tunnel/routes.ts index 6eadcf9..c8f6102 100644 --- a/src/backend/hosts/tunnel/routes.ts +++ b/src/backend/hosts/tunnel/routes.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../../utils/error-message.js"; import express, { type Response } from "express"; import axios from "axios"; @@ -7,6 +8,11 @@ import type { AuthenticatedRequest, } from "../../../types/index.js"; import { CONNECTION_STATES } from "../../../types/index.js"; +import { + logAudit, + getAuditUsername, + getRequestMeta, +} from "../../utils/audit-logger.js"; import { tunnelLogger } from "../../utils/logger.js"; import { SystemCrypto } from "../../utils/system-crypto.js"; import { AuthManager } from "../../utils/auth-manager.js"; @@ -30,7 +36,8 @@ import { handleDisconnect, sendTunnelStatusSnapshot, isSingleHostTunnel, - getAllTunnelStatus, + getTunnelStatusForUser, + canAccessTunnel, findHostByTunnelEndpoint, connectSSHTunnel, } from "./manager.js"; @@ -44,12 +51,12 @@ export function registerTunnelRoutes(app: express.Express): void { app.get( "/ssh/tunnel/status", authenticateJWT, - (req: AuthenticatedRequest, res: Response) => { + async (req: AuthenticatedRequest, res: Response) => { if (!req.userId) { return res.status(401).json({ error: "Authentication required" }); } - res.json(getAllTunnelStatus()); + res.json(await getTunnelStatusForUser(req.userId)); }, ); @@ -69,7 +76,7 @@ export function registerTunnelRoutes(app: express.Express): void { }); res.flushHeaders?.(); - tunnelStatusClients.add(res); + tunnelStatusClients.set(res, req.userId); sendTunnelStatusSnapshot(res); const heartbeat = setInterval(() => { @@ -112,7 +119,7 @@ export function registerTunnelRoutes(app: express.Express): void { app.get( "/ssh/tunnel/status/:tunnelName", authenticateJWT, - (req: AuthenticatedRequest, res: Response) => { + async (req: AuthenticatedRequest, res: Response) => { if (!req.userId) { return res.status(401).json({ error: "Authentication required" }); } @@ -121,6 +128,13 @@ export function registerTunnelRoutes(app: express.Express): void { const tunnelName = Array.isArray(tunnelNameParam) ? tunnelNameParam[0] : tunnelNameParam; + + // 404 rather than 403 for foreign tunnels: the name itself carries + // host metadata, so confirming its existence would leak it. + if (!(await canAccessTunnel(req.userId, tunnelName))) { + return res.status(404).json({ error: "Tunnel not found" }); + } + const status = connectionStatus.get(tunnelName); if (!status) { @@ -331,10 +345,7 @@ export function registerTunnelRoutes(app: express.Express): void { { operation: "tunnel_endpoint_credential_resolve", endpointHostId: endpointHost.id, - error: - credError instanceof Error - ? credError.message - : "Unknown", + error: getErrorMessage(credError, "Unknown"), }, ); } @@ -351,7 +362,7 @@ export function registerTunnelRoutes(app: express.Express): void { }, ); throw new Error( - `Failed to resolve endpoint host: ${resolveError instanceof Error ? resolveError.message : "Unknown error"}`, + `Failed to resolve endpoint host: ${getErrorMessage(resolveError)}`, { cause: resolveError }, ); } @@ -363,6 +374,26 @@ export function registerTunnelRoutes(app: express.Express): void { pendingTunnelOperations.set(tunnelName, operation); + const { ipAddress, userAgent } = getRequestMeta(req); + await logAudit({ + userId, + username: await getAuditUsername(userId), + action: "tunnel_connect", + resourceType: "tunnel", + resourceId: tunnelConfig.sourceHostId + ? String(tunnelConfig.sourceHostId) + : undefined, + resourceName: tunnelName, + details: JSON.stringify({ + endpointHost: tunnelConfig.endpointHost, + endpointPort: tunnelConfig.endpointPort, + sourcePort: tunnelConfig.sourcePort, + }), + ipAddress, + userAgent, + success: true, + }); + res.json({ message: "Connection request received", tunnelName }); operation @@ -374,7 +405,7 @@ export function registerTunnelRoutes(app: express.Express): void { broadcastTunnelStatus(tunnelName, { connected: false, status: CONNECTION_STATES.FAILED, - reason: err instanceof Error ? err.message : "Unknown error", + reason: getErrorMessage(err), }); tunnelConnecting.delete(tunnelName); }) diff --git a/src/backend/services/dashboard.ts b/src/backend/services/dashboard.ts index 6c2d903..dc94f31 100644 --- a/src/backend/services/dashboard.ts +++ b/src/backend/services/dashboard.ts @@ -1,6 +1,7 @@ import express from "express"; import cookieParser from "cookie-parser"; import { createCorsMiddleware } from "../utils/cors-config.js"; +import { createCompressionMiddleware } from "../utils/compression-config.js"; import { dashboardLogger } from "../utils/logger.js"; import { AuthManager } from "../utils/auth-manager.js"; import type { AuthenticatedRequest } from "../../types/index.js"; @@ -25,6 +26,7 @@ function isUserDataUnlocked(userId: string): boolean { return DataCrypto.getUserDataKey(userId) !== null; } +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware()); app.use(cookieParser()); app.use(express.json({ limit: "1mb" })); diff --git a/src/backend/services/homepage.ts b/src/backend/services/homepage.ts index 12e3b8b..de61e0b 100644 --- a/src/backend/services/homepage.ts +++ b/src/backend/services/homepage.ts @@ -1,6 +1,7 @@ import express from "express"; import cookieParser from "cookie-parser"; import { createCorsMiddleware } from "../utils/cors-config.js"; +import { createCompressionMiddleware } from "../utils/compression-config.js"; import { AuthManager } from "../utils/auth-manager.js"; import { homepageItemsRouter } from "../database/routes/homepage-items-routes.js"; import { homepageLayoutRouter } from "../database/routes/homepage-layout-routes.js"; @@ -13,6 +14,7 @@ const app = express(); const authManager = AuthManager.getInstance(); const PORT = 30012; +app.use(createCompressionMiddleware()); app.use(createCorsMiddleware()); app.use(cookieParser()); app.use(express.json({ limit: "1mb" })); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index dfc0a8f..5f15411 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -1,6 +1,6 @@ +import { getErrorMessage } from "./utils/error-message.js"; import dotenv from "dotenv"; -import { promises as fs } from "fs"; -import { readFileSync } from "fs"; +import { promises as fs, readFileSync } from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { AutoSSLSetup } from "./utils/auto-ssl-setup.js"; @@ -14,6 +14,91 @@ import { versionLogger, setGlobalLogLevel, } from "./utils/logger.js"; +import { getTrustedProxyAuthConfig } from "./utils/trusted-proxy-auth.js"; + +/** + * host:port from DATABASE_URL for the startup log. Parsed rather than printed + * so the password the URL also carries never reaches the logs. + */ +function describeDatabaseHost(): string { + const raw = process.env.DATABASE_URL?.trim(); + if (!raw) return "unknown"; + + try { + const { host } = new URL(raw); + return host || "unknown"; + } catch { + return "unknown"; + } +} + +async function provisionLocalDesktopUserIfNeeded(): Promise { + const { createCurrentUserRepository, createCurrentRoleRepository } = + await import("./database/repositories/factory.js"); + const { AuthManager } = await import("./utils/auth-manager.js"); + const crypto = await import("crypto"); + + const userRepository = createCurrentUserRepository(); + const existingCount = await userRepository.countAll(); + if (existingCount > 0) { + const allUsers = await userRepository.listAll(); + for (const user of allUsers) { + try { + await AuthManager.getInstance().registerUser(user.id); + } catch (dekError) { + systemLogger.error( + "Failed to verify/provision data-encryption key for existing user", + dekError, + { operation: "desktop_dek_healing", userId: user.id }, + ); + } + } + return; + } + + const id = crypto.randomUUID(); + const { isFirstUser } = await userRepository.createFirstLocalUser({ + id, + username: "local", + passwordHash: "", + isOidc: false, + clientId: "", + clientSecret: "", + issuerUrl: "", + authorizationUrl: "", + tokenUrl: "", + identifierPath: "", + namePath: "", + scopes: "openid email profile", + totpSecret: null, + totpEnabled: false, + totpBackupCodes: null, + }); + + try { + await createCurrentRoleRepository().assignRoleNameToUser({ + userId: id, + roleName: isFirstUser ? "admin" : "user", + grantedBy: id, + }); + } catch (roleError) { + systemLogger.error( + "Failed to assign default role to auto-provisioned local user", + roleError, + { operation: "desktop_auto_provision_role" }, + ); + } + + await AuthManager.getInstance().registerUser( + id, + crypto.randomBytes(32).toString("hex"), + ); + + systemLogger.success("Auto-provisioned local desktop user", { + operation: "desktop_auto_provision", + userId: id, + }); +} (async () => { const initStartTime = Date.now(); @@ -61,18 +146,39 @@ import { } } } + process.env.VERSION = version; + versionLogger.info(`Termix Backend starting - Version: ${version}`, { operation: "startup", version: version, }); + const trustedProxyAuth = getTrustedProxyAuthConfig(); + const systemCrypto = SystemCrypto.getInstance(); await systemCrypto.initializeJWTSecret(); await systemCrypto.initializeDatabaseKey(); await systemCrypto.initializeEncryptionKey(); await systemCrypto.initializeInternalAuthToken(); - ensureDatabaseLayerPreupgradeBackup({ dataDir, version }); + const { needsExplicitPersist, resolveDatabaseDialect } = + await import("./database/db/dialect.js"); + const databaseDialect = resolveDatabaseDialect(); + + // The pre-upgrade backup copies the SQLite file, so there is nothing for it + // to do on a client-server engine. Say so rather than no-op silently: + // backups are the operator's own responsibility there. + if (needsExplicitPersist(databaseDialect)) { + ensureDatabaseLayerPreupgradeBackup({ dataDir, version }); + } else { + systemLogger.info( + `Skipping pre-upgrade backup on ${databaseDialect} - back up the database yourself`, + { + operation: "backend_init_db_backup_skipped", + dialect: databaseDialect, + }, + ); + } await AutoSSLSetup.initialize(); systemLogger.success("SSL setup completed", { @@ -82,10 +188,52 @@ import { const dbModule = await import("./database/db/index.js"); await dbModule.initializeDatabase(); - systemLogger.success("Database initialized", { + // Naming the engine makes a misconfiguration obvious: without it, a bad + // DATABASE_DIALECT silently falls back to SQLite and looks like data loss. + systemLogger.success(`Database initialized (${databaseDialect})`, { operation: "backend_init_db", + dialect: databaseDialect, + // Host only, never the credentials the URL also carries. + ...(needsExplicitPersist(databaseDialect) + ? {} + : { host: describeDatabaseHost() }), }); + if (trustedProxyAuth.enabled) { + const { + createCurrentSettingsRepository, + createCurrentSsoProviderRepository, + createCurrentUserRepository, + } = await import("./database/repositories/factory.js"); + const [legacyOidc, providers, users] = await Promise.all([ + createCurrentSettingsRepository().get("oidc_config"), + createCurrentSsoProviderRepository().listEnabled(), + createCurrentUserRepository().listAll(), + ]); + const conflictingProvider = providers.some((provider) => + ["oidc", "github", "google"].includes(provider.type), + ); + const conflictingUser = users.some( + (user) => user.isOidc || user.totpEnabled, + ); + if ( + legacyOidc || + process.env.OIDC_CLIENT_ID || + conflictingProvider || + conflictingUser + ) { + throw new Error( + "Trusted proxy authentication cannot start while OIDC or TOTP is enabled", + ); + } + systemLogger.info("Trusted proxy authentication enabled", { + operation: "trusted_proxy_auth_enabled", + usernameHeader: trustedProxyAuth.usernameHeader, + roleHeader: trustedProxyAuth.roleHeader, + trustedProxyCount: trustedProxyAuth.trustedProxies.length, + }); + } + const { UserKeyManager } = await import("./utils/user-keys.js"); await UserKeyManager.getInstance().initialize(); @@ -101,10 +249,30 @@ import { await authManager.initialize(); DataCrypto.initialize(); + const { runLegacySharedSshAuthOptInMigration } = + await import("./utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.js"); + await runLegacySharedSshAuthOptInMigration(); + const { runSharedHostSecretsMigration } = await import("./utils/crypto-migration/shared-host-secrets-migration.js"); await runSharedHostSecretsMigration(); + const { runPrivateSharedSshAuthMigration } = + await import("./utils/crypto-migration/private-shared-ssh-auth-migration.js"); + await runPrivateSharedSshAuthMigration(); + + const { runChannelConfigEncryptionMigration } = + await import("./utils/crypto-migration/channel-config-encryption.js"); + await runChannelConfigEncryptionMigration(); + + const { runAutomationsMigration } = + await import("./utils/crypto-migration/automations-migration.js"); + await runAutomationsMigration(); + + if (process.env.ELECTRON_EMBEDDED === "true") { + await provisionLocalDesktopUserIfNeeded(); + } + import("./utils/opkssh-binary-manager.js").then( ({ OPKSSHBinaryManager }) => { OPKSSHBinaryManager.ensureBinary().catch((error) => { @@ -114,7 +282,7 @@ import { "Failed to initialize OPKSSH binary - OPKSSH authentication will not be available", { operation: "opkssh_binary_init_failed", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), stack: error instanceof Error ? error.stack : undefined, platform: process.platform, arch: process.arch, @@ -164,12 +332,20 @@ import { "Failed to initialize Guacamole server (guacd may not be available)", { operation: "guac_init_skip", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }, ); }); } + // After metrics, which the automation triggers and headless polling hook into. + const { startAutomationScheduler } = + await import("./automations/scheduler.js"); + startAutomationScheduler(); + + const { startAnalyticsHeartbeat } = await import("./utils/analytics.js"); + startAnalyticsHeartbeat(); + systemLogger.success("Termix backend started successfully", { operation: "backend_init_complete", port: process.env.PORT || 4090, @@ -181,15 +357,20 @@ import { systemLogger.info(`Received ${signal}, initiating graceful shutdown...`, { operation: "shutdown", }); - try { - await DatabaseSaveTrigger.forceSave("shutdown_explicit_save"); - systemLogger.info("Database saved to disk before exit", { - operation: "shutdown_db_saved", - }); - } catch (error) { - systemLogger.error("Failed to save database during shutdown", error, { - operation: "shutdown_db_save_failed", - }); + // Only SQLite has anything to flush. On a client-server engine the writes + // committed as they happened, so there is no file to save and claiming + // otherwise in the log would be untrue. + if (needsExplicitPersist(databaseDialect)) { + try { + await DatabaseSaveTrigger.forceSave("shutdown_explicit_save"); + systemLogger.info("Database saved to disk before exit", { + operation: "shutdown_db_saved", + }); + } catch (error) { + systemLogger.error("Failed to save database during shutdown", error, { + operation: "shutdown_db_save_failed", + }); + } } process.exit(0); }; diff --git a/src/backend/tests/ai/command-allowlist.test.ts b/src/backend/tests/ai/command-allowlist.test.ts new file mode 100644 index 0000000..a901cd9 --- /dev/null +++ b/src/backend/tests/ai/command-allowlist.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { isReadOnlyCommand } from "../../ai/tools/command-allowlist.js"; + +/** + * These commands can run without a per-command approval click, so the parser + * has to refuse anything that could become a second command. Substring + * matching would pass "df; rm -rf /", which is the whole reason this is + * argv-based. + */ +describe("isReadOnlyCommand", () => { + it("allows plain diagnostics", () => { + for (const command of [ + "df -h", + "uptime", + "free -m", + "whoami", + "ps aux", + "lsblk", + "uname -a", + "/usr/bin/df -h", + ]) { + expect(isReadOnlyCommand(command).allowed, command).toBe(true); + } + }); + + it("rejects command chaining and redirection", () => { + for (const command of [ + "df; rm -rf /", + "df && curl evil.example", + "df || reboot", + "df | sh", + "df > /etc/passwd", + "df >> /etc/passwd", + "cat < /etc/shadow", + "echo `whoami`", + "echo $(id)", + "df\nrm -rf /", + "df \\\n rm", + ]) { + expect(isReadOnlyCommand(command).allowed, command).toBe(false); + } + }); + + it("rejects privilege escalation", () => { + for (const command of ["sudo df -h", "su root", "doas df", "env df"]) { + expect(isReadOnlyCommand(command).allowed, command).toBe(false); + } + }); + + it("rejects commands that are not on the list", () => { + for (const command of ["rm -rf /", "curl evil.example", "vi /etc/passwd"]) { + expect(isReadOnlyCommand(command).allowed, command).toBe(false); + } + }); + + it("limits systemctl and docker to read-only subcommands", () => { + expect(isReadOnlyCommand("systemctl status nginx").allowed).toBe(true); + expect(isReadOnlyCommand("systemctl restart nginx").allowed).toBe(false); + expect(isReadOnlyCommand("systemctl stop nginx").allowed).toBe(false); + + expect(isReadOnlyCommand("docker ps").allowed).toBe(true); + expect(isReadOnlyCommand("docker stats --no-stream").allowed).toBe(true); + expect(isReadOnlyCommand("docker rm -f web").allowed).toBe(false); + expect(isReadOnlyCommand("docker exec -it web sh").allowed).toBe(false); + }); + + it("limits cat to safe paths", () => { + expect(isReadOnlyCommand("cat /proc/meminfo").allowed).toBe(true); + expect(isReadOnlyCommand("cat /etc/os-release").allowed).toBe(true); + expect(isReadOnlyCommand("cat /etc/shadow").allowed).toBe(false); + expect(isReadOnlyCommand("cat ~/.ssh/id_rsa").allowed).toBe(false); + expect(isReadOnlyCommand("cat").allowed).toBe(false); + }); + + it("rejects an empty command", () => { + expect(isReadOnlyCommand("").allowed).toBe(false); + expect(isReadOnlyCommand(" ").allowed).toBe(false); + }); +}); diff --git a/src/backend/tests/ai/context.test.ts b/src/backend/tests/ai/context.test.ts new file mode 100644 index 0000000..a800fdf --- /dev/null +++ b/src/backend/tests/ai/context.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { buildSystemPrompt } from "../../ai/context.js"; + +const BASE = { hostCount: 3, allowReadOnlyCommands: false }; + +describe("buildSystemPrompt", () => { + it("tells the assistant to stay inside what was asked", () => { + // Without these the assistant answered "what is running on this server" + // by also proposing a monitoring script and an alert rule nobody wanted. + const prompt = buildSystemPrompt(BASE); + + expect(prompt).toContain("nothing beyond it"); + expect(prompt).toMatch(/Read, then report\. Do not propose anything\./); + expect(prompt).toMatch(/no monitoring, no alert rules, no scripts/); + expect(prompt).toMatch(/One request means one proposal at most/); + }); + + it("states that it proposes rather than applies", () => { + const prompt = buildSystemPrompt(BASE); + expect(prompt).toContain("cannot change anything directly"); + expect(prompt).toContain("Never claim you have done something"); + }); + + it("never claims credential access", () => { + const prompt = buildSystemPrompt(BASE); + expect(prompt).toMatch(/no access to passwords, SSH keys, API keys/); + }); + + it("mentions read-only commands only when the user opted in", () => { + expect(buildSystemPrompt(BASE)).not.toMatch(/read-only diagnostic/); + expect(buildSystemPrompt({ ...BASE, allowReadOnlyCommands: true })).toMatch( + /read-only diagnostic/, + ); + }); + + it("pluralises the host count", () => { + expect(buildSystemPrompt({ ...BASE, hostCount: 1 })).toContain("1 host "); + expect(buildSystemPrompt({ ...BASE, hostCount: 2 })).toContain("2 hosts"); + }); + + it("includes the active tab only when there is one", () => { + expect(buildSystemPrompt(BASE)).not.toContain("currently looking at"); + expect(buildSystemPrompt({ ...BASE, activeTab: "terminal" })).toContain( + "currently looking at: terminal", + ); + }); +}); diff --git a/src/backend/tests/ai/egress.test.ts b/src/backend/tests/ai/egress.test.ts new file mode 100644 index 0000000..577f94e --- /dev/null +++ b/src/backend/tests/ai/egress.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest"; +import { + DEFAULT_PRIVATE_ALLOWLIST, + evaluateEgress, + isPrivateDestination, + parseAllowlist, +} from "../../ai/egress.js"; + +/** + * The rule that lets a self-hosted Ollama work without turning the backend + * into an authenticated probe of its own network: private destinations are + * refused unless an admin named the host. + */ +describe("isPrivateDestination", () => { + it("recognises loopback and private ranges", () => { + for (const url of [ + "http://localhost:11434", + "http://127.0.0.1:11434", + "http://10.0.0.5:11434", + "http://192.168.1.10:11434", + "http://172.16.4.4:11434", + "http://169.254.169.254/latest/meta-data", + "http://[::1]:11434", + ]) { + expect(isPrivateDestination(url), url).toBe(true); + } + }); + + it("treats public hosts as public", () => { + for (const url of [ + "https://api.openai.com/v1", + "https://api.anthropic.com", + "https://generativelanguage.googleapis.com", + "http://8.8.8.8", + ]) { + expect(isPrivateDestination(url), url).toBe(false); + } + }); +}); + +describe("evaluateEgress", () => { + it("allows public destinations without an allowlist entry", () => { + const decision = evaluateEgress("https://api.openai.com/v1", []); + expect(decision.allowed).toBe(true); + expect(decision.isPrivate).toBe(false); + }); + + it("refuses a private destination that is not allowlisted", () => { + const decision = evaluateEgress("http://192.168.1.50:11434", ["localhost"]); + expect(decision.allowed).toBe(false); + expect(decision.isPrivate).toBe(true); + expect(decision.reason).toContain("allowlist"); + }); + + it("allows a private destination once its host is allowlisted", () => { + const decision = evaluateEgress("http://localhost:11434", [ + "localhost", + "127.0.0.1", + ]); + expect(decision.allowed).toBe(true); + expect(decision.isPrivate).toBe(true); + }); + + it("matches the allowlist case-insensitively", () => { + expect( + evaluateEgress("http://LOCALHOST:11434", ["localhost"]).allowed, + ).toBe(true); + }); + + it("does not let one allowlisted host authorise another", () => { + // A cloud metadata endpoint is the classic target, so an allowlist for + // localhost must not open 169.254.169.254. + const decision = evaluateEgress("http://169.254.169.254/latest/meta-data", [ + "localhost", + "127.0.0.1", + ]); + expect(decision.allowed).toBe(false); + }); + + it("refuses non-http protocols and embedded credentials", () => { + expect(evaluateEgress("file:///etc/passwd", []).allowed).toBe(false); + expect(evaluateEgress("ftp://example.com", []).allowed).toBe(false); + expect(evaluateEgress("https://user:pass@api.openai.com", []).allowed).toBe( + false, + ); + }); + + it("refuses a malformed url", () => { + expect(evaluateEgress("not a url", []).allowed).toBe(false); + }); +}); + +describe("parseAllowlist", () => { + it("falls back to the defaults when unset or malformed", () => { + expect(parseAllowlist(null)).toEqual(DEFAULT_PRIVATE_ALLOWLIST); + expect(parseAllowlist("not json")).toEqual(DEFAULT_PRIVATE_ALLOWLIST); + expect(parseAllowlist('{"a":1}')).toEqual(DEFAULT_PRIVATE_ALLOWLIST); + }); + + it("normalises stored entries", () => { + expect(parseAllowlist('[" LocalHost ", "", "10.0.0.5"]')).toEqual([ + "localhost", + "10.0.0.5", + ]); + }); + + it("honours a deliberately empty allowlist", () => { + // An admin clearing the list must actually block everything private, + // not silently get the defaults back. + expect(parseAllowlist("[]")).toEqual([]); + }); +}); diff --git a/src/backend/tests/ai/engine.test.ts b/src/backend/tests/ai/engine.test.ts new file mode 100644 index 0000000..85bb40c --- /dev/null +++ b/src/backend/tests/ai/engine.test.ts @@ -0,0 +1,208 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ChatChunk } from "../../ai/providers/types.js"; + +const streamChat = vi.fn(); +const handler = vi.fn(); + +vi.mock("../../ai/providers/registry.js", () => ({ + getAdapter: () => ({ streamChat, listModels: async () => [] }), +})); + +vi.mock("../../ai/tools/catalog.js", () => ({ + getTool: (name: string) => + name === "list_hosts" + ? { + name: "list_hosts", + description: "List hosts", + category: "read", + parameters: { type: "object", properties: {} }, + handler, + } + : undefined, + toolDefinitions: () => [ + { name: "list_hosts", description: "List hosts", parameters: {} }, + ], +})); + +const { runAgent } = await import("../../ai/engine.js"); + +function chunks(...values: ChatChunk[]) { + return (async function* () { + for (const value of values) yield value; + })(); +} + +const BASE = { + config: { providerType: "ollama" as const }, + model: "test", + system: "system", + context: { + userId: "user-1", + conversationId: 1, + allowReadOnlyCommands: false, + }, +}; + +async function collect(history: any[] = []) { + const events: any[] = []; + for await (const event of runAgent({ ...BASE, history })) { + events.push(event); + } + return events; +} + +describe("runAgent", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("streams text and finishes when no tools are called", async () => { + streamChat.mockReturnValueOnce( + chunks({ type: "text", text: "hello" }, { type: "done" }), + ); + + const events = await collect(); + + expect(events.filter((e) => e.type === "token")).toHaveLength(1); + expect(events.at(-1).type).toBe("done"); + }); + + it("runs a known tool and feeds the result back", async () => { + handler.mockResolvedValue({ hosts: [{ id: 1, name: "web-1" }] }); + streamChat + .mockReturnValueOnce( + chunks( + { + type: "tool_call", + call: { id: "c1", name: "list_hosts", arguments: {} }, + }, + { type: "done" }, + ), + ) + .mockReturnValueOnce( + chunks({ type: "text", text: "ok" }, { type: "done" }), + ); + + const events = await collect(); + + expect(handler).toHaveBeenCalledOnce(); + expect(events.some((e) => e.type === "tool_result")).toBe(true); + expect(streamChat).toHaveBeenCalledTimes(2); + }); + + it("refuses a tool that is not in the catalog", async () => { + streamChat + .mockReturnValueOnce( + chunks( + { + type: "tool_call", + call: { id: "c1", name: "read_credentials", arguments: {} }, + }, + { type: "done" }, + ), + ) + .mockReturnValueOnce( + chunks({ type: "text", text: "ok" }, { type: "done" }), + ); + + const events = await collect(); + + // A model can emit any name it likes; only the catalog decides what runs. + expect(handler).not.toHaveBeenCalled(); + const result = events.find((e) => e.type === "tool_result"); + expect(JSON.stringify(result.result)).toContain("Unknown tool"); + }); + + it("surfaces a handler failure without ending the run", async () => { + handler.mockRejectedValue(new Error("database is down")); + streamChat + .mockReturnValueOnce( + chunks( + { + type: "tool_call", + call: { id: "c1", name: "list_hosts", arguments: {} }, + }, + { type: "done" }, + ), + ) + .mockReturnValueOnce( + chunks({ type: "text", text: "ok" }, { type: "done" }), + ); + + const events = await collect(); + + const result = events.find((e) => e.type === "tool_result"); + expect(JSON.stringify(result.result)).toContain("database is down"); + expect(events.at(-1).type).toBe("done"); + }); + + it("closes the tool call when a tool returns a proposal", async () => { + // Without a matching tool_result the call rendered as permanently + // running, even though the work was done and awaiting the user. + handler.mockResolvedValue({ + __proposal: true, + kind: "propose_create_host", + summary: "Add host web-1", + payload: {}, + }); + streamChat + .mockReturnValueOnce( + chunks( + { + type: "tool_call", + call: { id: "c1", name: "list_hosts", arguments: {} }, + }, + { type: "done" }, + ), + ) + .mockReturnValueOnce( + chunks({ type: "text", text: "ok" }, { type: "done" }), + ); + + const events = await collect(); + + const callIndex = events.findIndex((e) => e.type === "tool_call"); + const resultIndex = events.findIndex((e) => e.type === "tool_result"); + const proposalIndex = events.findIndex((e) => e.type === "proposal"); + + expect(resultIndex).toBeGreaterThan(callIndex); + expect(proposalIndex).toBeGreaterThan(resultIndex); + expect(events[resultIndex]).toMatchObject({ + name: "list_hosts", + result: { status: "awaiting_user_approval" }, + }); + }); + + it("reports a provider failure as an error and stops", async () => { + streamChat.mockImplementationOnce(() => { + throw new Error("provider unreachable"); + }); + + const events = await collect(); + + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + type: "error", + message: "provider unreachable", + }); + }); + + it("stops after too many tool turns", async () => { + handler.mockResolvedValue({ ok: true }); + streamChat.mockImplementation(() => + chunks( + { + type: "tool_call", + call: { id: "c", name: "list_hosts", arguments: {} }, + }, + { type: "done" }, + ), + ); + + const events = await collect(); + + // A model that never stops calling tools must not spin forever. + expect(events.at(-1)).toMatchObject({ type: "error" }); + expect(streamChat.mock.calls.length).toBeLessThanOrEqual(8); + }); +}); diff --git a/src/backend/tests/ai/gating.test.ts b/src/backend/tests/ai/gating.test.ts new file mode 100644 index 0000000..32fd7c9 --- /dev/null +++ b/src/backend/tests/ai/gating.test.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const settingsRepository = { getBoolean: vi.fn() }; +const userPreferenceRepository = { findByUserId: vi.fn() }; + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentSettingsRepository: () => settingsRepository, + createCurrentUserPreferenceRepository: () => userPreferenceRepository, +})); + +const { isAiGloballyEnabled, resolveAiAccess } = + await import("../../ai/gating.js"); + +describe("AI gating", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults to off so upgrading an install enables nothing", async () => { + settingsRepository.getBoolean.mockResolvedValue(false); + await isAiGloballyEnabled(); + expect(settingsRepository.getBoolean).toHaveBeenCalledWith( + "ai_globally_enabled", + false, + ); + }); + + it("blocks everyone when the admin global is off", async () => { + settingsRepository.getBoolean.mockResolvedValue(false); + userPreferenceRepository.findByUserId.mockResolvedValue({ + aiAssistantEnabled: true, + aiReadOnlyCommands: true, + }); + + const access = await resolveAiAccess("user-1"); + + expect(access.enabled).toBe(false); + expect(access.allowReadOnlyCommands).toBe(false); + // The kill switch short-circuits, so the preference is never consulted. + expect(userPreferenceRepository.findByUserId).not.toHaveBeenCalled(); + }); + + it("blocks a user who has not enabled it", async () => { + settingsRepository.getBoolean.mockResolvedValue(true); + userPreferenceRepository.findByUserId.mockResolvedValue({ + aiAssistantEnabled: false, + }); + + expect((await resolveAiAccess("user-1")).enabled).toBe(false); + }); + + it("treats never-asked as not enabled", async () => { + // Null means the user was never shown the choice, which is not consent. + settingsRepository.getBoolean.mockResolvedValue(true); + userPreferenceRepository.findByUserId.mockResolvedValue({ + aiAssistantEnabled: null, + }); + + expect((await resolveAiAccess("user-1")).enabled).toBe(false); + }); + + it("treats a missing preference row as not enabled", async () => { + settingsRepository.getBoolean.mockResolvedValue(true); + userPreferenceRepository.findByUserId.mockResolvedValue(null); + + expect((await resolveAiAccess("user-1")).enabled).toBe(false); + }); + + it("allows only when both gates are open", async () => { + settingsRepository.getBoolean.mockResolvedValue(true); + userPreferenceRepository.findByUserId.mockResolvedValue({ + aiAssistantEnabled: true, + aiReadOnlyCommands: true, + }); + + const access = await resolveAiAccess("user-1"); + + expect(access.enabled).toBe(true); + expect(access.allowReadOnlyCommands).toBe(true); + }); + + it("keeps read-only commands off unless separately opted in", async () => { + settingsRepository.getBoolean.mockResolvedValue(true); + userPreferenceRepository.findByUserId.mockResolvedValue({ + aiAssistantEnabled: true, + aiReadOnlyCommands: null, + }); + + const access = await resolveAiAccess("user-1"); + + expect(access.enabled).toBe(true); + expect(access.allowReadOnlyCommands).toBe(false); + }); +}); diff --git a/src/backend/tests/ai/proposal-executor.test.ts b/src/backend/tests/ai/proposal-executor.test.ts new file mode 100644 index 0000000..06d32ff --- /dev/null +++ b/src/backend/tests/ai/proposal-executor.test.ts @@ -0,0 +1,255 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const hostRepository = { + create: vi.fn(), + findByIdForUser: vi.fn(), + updateForUser: vi.fn(), + deleteForUser: vi.fn(), +}; +const snippetRepository = { + createSnippet: vi.fn(), + findOwnedById: vi.fn(), + updateSnippet: vi.fn(), + deleteSnippet: vi.fn(), +}; +const fleetRepository = { create: vi.fn(), addMember: vi.fn() }; +const alertRepository = { createAlertRule: vi.fn() }; +const automationRepository = { create: vi.fn() }; + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentHostRepository: () => hostRepository, + createCurrentSnippetRepository: () => snippetRepository, + createCurrentFleetRepository: () => fleetRepository, + createCurrentAlertRepository: () => alertRepository, + createCurrentAutomationRepository: () => automationRepository, +})); + +const resolveHostById = vi.fn(); +vi.mock("../../hosts/host-resolver.js", () => ({ + resolveHostById: (...args: unknown[]) => resolveHostById(...args), +})); + +const execCommand = vi.fn(); +vi.mock("../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: (...args: unknown[]) => execCommand(...args), +})); +vi.mock("../../hosts/ssh-client-factory.js", () => ({ + createFleetSshFactory: () => () => ({}), + getFleetPoolKey: () => "pool", +})); +vi.mock("../../hosts/ssh-connection-pool.js", () => ({ + withConnection: async ( + _key: string, + _factory: unknown, + run: (client: unknown) => Promise, + ) => run({}), +})); + +const { applyProposal } = await import("../../ai/tools/executor.js"); + +describe("applyProposal", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("refuses a kind that is not a real tool", async () => { + // The stored payload is treated as untrusted even though the server wrote + // it, because a proposal can outlive the release that created it. + await expect( + applyProposal("propose_delete_everything", {}, "user-1"), + ).rejects.toThrow("Unknown proposal kind"); + }); + + it("validates an automation definition before creating it", async () => { + // Reuses the automations route's own validator, so a definition the model + // invented is held to the same standard as a hand-written one. + await expect( + applyProposal( + "propose_create_automation", + { + name: "bad", + definition: { trigger: { kind: "nonsense" }, steps: [] }, + }, + "user-1", + ), + ).rejects.toThrow(); + expect(automationRepository.create).not.toHaveBeenCalled(); + }); + + it("creates a valid automation disabled so it cannot fire unwatched", async () => { + automationRepository.create.mockResolvedValue({ id: 5, name: "nightly" }); + + const result = await applyProposal( + "propose_create_automation", + { + name: "nightly", + definition: { + trigger: { kind: "schedule", intervalSeconds: 3600 }, + steps: [{ id: "s1", type: "wait", seconds: 1 }], + }, + }, + "user-1", + ); + + expect(result.ok).toBe(true); + expect(automationRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ userId: "user-1", enabled: false }), + ); + }); + + it("runs an approved command only on a host the user can reach", async () => { + // resolveHostById returns null when the connect-level permission check + // fails, so an unreachable host never gets as far as an SSH attempt. + resolveHostById.mockResolvedValue(null); + + await expect( + applyProposal( + "propose_run_command", + { hostId: 9, command: "uptime" }, + "user-1", + ), + ).rejects.toThrow("Host not found"); + expect(execCommand).not.toHaveBeenCalled(); + }); + + it("returns command output on success", async () => { + resolveHostById.mockResolvedValue({ id: 9, ip: "10.0.0.9" }); + execCommand.mockResolvedValue({ stdout: "up 3 days", stderr: "", code: 0 }); + + const result = await applyProposal( + "propose_run_command", + { hostId: 9, command: "uptime" }, + "user-1", + ); + + expect(result.ok).toBe(true); + expect(result.summary).toContain("up 3 days"); + }); + + it("resolves the host rather than using a raw repository row", async () => { + // A raw row has no decrypted auth and an unresolved jumpHosts field, which + // made the SSH factory fail with a jump host error on hosts that have none. + resolveHostById.mockResolvedValue({ id: 9, ip: "10.0.0.9" }); + execCommand.mockResolvedValue({ stdout: "ok", stderr: "", code: 0 }); + + await applyProposal( + "propose_run_command", + { hostId: 9, command: "uptime" }, + "user-1", + ); + + expect(resolveHostById).toHaveBeenCalledWith(9, "user-1"); + expect(hostRepository.findByIdForUser).not.toHaveBeenCalled(); + }); + + it("surfaces a non-zero exit rather than reporting success", async () => { + resolveHostById.mockResolvedValue({ id: 9, ip: "10.0.0.9" }); + execCommand.mockResolvedValue({ stdout: "", stderr: "denied", code: 1 }); + + await expect( + applyProposal( + "propose_run_command", + { hostId: 9, command: "cat /etc/shadow" }, + "user-1", + ), + ).rejects.toThrow("code 1"); + }); + + it("creates a host through the normal repository", async () => { + hostRepository.create.mockResolvedValue({ id: 7, name: "web-1" }); + + const result = await applyProposal( + "propose_create_host", + { name: "web-1", ip: "10.0.0.5", port: 22, tags: ["prod"] }, + "user-1", + ); + + expect(result.ok).toBe(true); + expect(hostRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + userId: "user-1", + name: "web-1", + ip: "10.0.0.5", + }), + ); + }); + + it("rejects a host payload missing required fields", async () => { + await expect( + applyProposal("propose_create_host", { name: "web-1" }, "user-1"), + ).rejects.toThrow("ip is required"); + expect(hostRepository.create).not.toHaveBeenCalled(); + }); + + it("scopes an update to the approving user", async () => { + hostRepository.findByIdForUser.mockResolvedValue({ id: 7 }); + hostRepository.updateForUser.mockResolvedValue({ id: 7 }); + + await applyProposal( + "propose_update_host", + { hostId: 7, changes: { name: "renamed" } }, + "user-1", + ); + + expect(hostRepository.findByIdForUser).toHaveBeenCalledWith("user-1", 7); + expect(hostRepository.updateForUser).toHaveBeenCalledWith( + "user-1", + 7, + expect.objectContaining({ name: "renamed" }), + ); + }); + + it("refuses to update a host the user does not own", async () => { + hostRepository.findByIdForUser.mockResolvedValue(null); + + await expect( + applyProposal( + "propose_update_host", + { hostId: 999, changes: { name: "x" } }, + "user-1", + ), + ).rejects.toThrow("Host not found"); + expect(hostRepository.updateForUser).not.toHaveBeenCalled(); + }); + + it("rejects a non-numeric id rather than coercing it", async () => { + await expect( + applyProposal( + "propose_update_host", + { hostId: "7; DROP TABLE hosts", changes: { name: "x" } }, + "user-1", + ), + ).rejects.toThrow("hostId must be a positive integer"); + }); + + it("only adds fleet members the approving user owns", async () => { + fleetRepository.create.mockResolvedValue({ id: 3, name: "prod" }); + hostRepository.findByIdForUser.mockImplementation( + async (_userId: string, hostId: number) => + hostId === 1 ? { id: 1 } : null, + ); + + const result = await applyProposal( + "propose_create_fleet", + { name: "prod", hostIds: [1, 2] }, + "user-1", + ); + + expect(fleetRepository.addMember).toHaveBeenCalledTimes(1); + expect(fleetRepository.addMember).toHaveBeenCalledWith(3, 1); + expect(result.summary).toContain("1 host"); + }); + + it("reports nothing to change on an empty update", async () => { + hostRepository.findByIdForUser.mockResolvedValue({ id: 7 }); + + const result = await applyProposal( + "propose_update_host", + { hostId: 7, changes: {} }, + "user-1", + ); + + expect(result.ok).toBe(false); + expect(hostRepository.updateForUser).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/ai/providers.test.ts b/src/backend/tests/ai/providers.test.ts new file mode 100644 index 0000000..54f19e2 --- /dev/null +++ b/src/backend/tests/ai/providers.test.ts @@ -0,0 +1,305 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ChatChunk } from "../../ai/providers/types.js"; + +const providerFetch = vi.fn(); + +vi.mock("../../ai/providers/http.js", async () => { + const actual = await vi.importActual< + typeof import("../../ai/providers/http.js") + >("../../ai/providers/http.js"); + return { ...actual, providerFetch }; +}); + +const { openAiAdapter } = await import("../../ai/providers/openai.js"); +const { ollamaAdapter } = await import("../../ai/providers/ollama.js"); +const { geminiAdapter } = await import("../../ai/providers/gemini.js"); + +/** Builds a Response whose body streams the given text chunks. */ +function streamingResponse(lines: string[]): Response { + const encoder = new TextEncoder(); + return { + ok: true, + status: 200, + body: { + getReader() { + let index = 0; + return { + async read() { + if (index >= lines.length) return { done: true, value: undefined }; + return { done: false, value: encoder.encode(lines[index++]) }; + }, + releaseLock() {}, + }; + }, + }, + } as unknown as Response; +} + +/** One SSE frame, built from an object so the JSON stays readable. */ +function sseFrame(payload: unknown): string { + return `data: ${JSON.stringify(payload)}\n`; +} + +async function collect(iterable: AsyncIterable) { + const chunks: ChatChunk[] = []; + for await (const chunk of iterable) chunks.push(chunk); + return chunks; +} + +const REQUEST = { + model: "test-model", + system: "system", + messages: [{ role: "user" as const, content: "hi" }], + tools: [], +}; + +describe("openAiAdapter", () => { + beforeEach(() => vi.clearAllMocks()); + + it("normalises streamed text", async () => { + providerFetch.mockResolvedValue( + streamingResponse([ + 'data: {"choices":[{"delta":{"content":"Hel"}}]}\n', + 'data: {"choices":[{"delta":{"content":"lo"}}]}\n', + "data: [DONE]\n", + ]), + ); + + const chunks = await collect( + openAiAdapter.streamChat({ providerType: "openai" }, REQUEST), + ); + + expect(chunks.filter((c) => c.type === "text")).toEqual([ + { type: "text", text: "Hel" }, + { type: "text", text: "lo" }, + ]); + expect(chunks.at(-1)?.type).toBe("done"); + }); + + it("reassembles tool arguments split across deltas", async () => { + providerFetch.mockResolvedValue( + streamingResponse([ + 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1","function":{"name":"list_hosts","arguments":"{\\"a"}}]}}]}\n', + 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":1}"}}]}}]}\n', + "data: [DONE]\n", + ]), + ); + + const chunks = await collect( + openAiAdapter.streamChat({ providerType: "openai" }, REQUEST), + ); + + const call = chunks.find((c) => c.type === "tool_call"); + expect(call).toMatchObject({ + type: "tool_call", + call: { id: "c1", name: "list_hosts", arguments: { a: 1 } }, + }); + }); + + it("survives malformed tool arguments", async () => { + // A model that emits broken JSON gets an empty object; the tool's own + // validation then reports the problem back to it. + providerFetch.mockResolvedValue( + streamingResponse([ + 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1","function":{"name":"list_hosts","arguments":"{not json"}}]}}]}\n', + "data: [DONE]\n", + ]), + ); + + const chunks = await collect( + openAiAdapter.streamChat({ providerType: "openai" }, REQUEST), + ); + + expect(chunks.find((c) => c.type === "tool_call")).toMatchObject({ + call: { arguments: {} }, + }); + }); + + it("needs a base url for an openai-compatible provider", async () => { + await expect( + collect( + openAiAdapter.streamChat( + { providerType: "openai_compatible" }, + REQUEST, + ), + ), + ).rejects.toThrow("base URL"); + }); +}); + +describe("ollamaAdapter", () => { + beforeEach(() => vi.clearAllMocks()); + + it("reads newline-delimited json rather than sse", async () => { + providerFetch.mockResolvedValue( + streamingResponse([ + '{"message":{"content":"Hel"}}\n', + '{"message":{"content":"lo"}}\n', + '{"done":true,"done_reason":"stop"}\n', + ]), + ); + + const chunks = await collect( + ollamaAdapter.streamChat({ providerType: "ollama" }, REQUEST), + ); + + expect(chunks.filter((c) => c.type === "text")).toHaveLength(2); + expect(chunks.at(-1)).toMatchObject({ type: "done", stopReason: "stop" }); + }); + + it("accepts tool arguments as an object or a json string", async () => { + providerFetch.mockResolvedValue( + streamingResponse([ + '{"message":{"tool_calls":[{"function":{"name":"list_hosts","arguments":{"a":1}}}]}}\n', + '{"message":{"tool_calls":[{"function":{"name":"get_host","arguments":"{\\"hostId\\":2}"}}]}}\n', + '{"done":true}\n', + ]), + ); + + const chunks = await collect( + ollamaAdapter.streamChat({ providerType: "ollama" }, REQUEST), + ); + + const calls = chunks.filter((c) => c.type === "tool_call") as any[]; + expect(calls[0].call.arguments).toEqual({ a: 1 }); + expect(calls[1].call.arguments).toEqual({ hostId: 2 }); + }); +}); + +describe("geminiAdapter", () => { + beforeEach(() => vi.clearAllMocks()); + + it("carries thoughtSignature off a function call", async () => { + // Gemini 2.5+ 400s a follow-up whose functionCall parts lost their + // signature, which broke every conversation on the second turn. + providerFetch.mockResolvedValue( + streamingResponse([ + sseFrame({ + candidates: [ + { + content: { + parts: [ + { + functionCall: { name: "list_hosts", args: {} }, + thoughtSignature: "sig-abc", + }, + ], + }, + }, + ], + }), + ]), + ); + + const chunks = await collect( + geminiAdapter.streamChat( + { providerType: "gemini", apiKey: "k" }, + REQUEST, + ), + ); + + expect(chunks.find((c) => c.type === "tool_call")).toMatchObject({ + call: { name: "list_hosts", providerSignature: "sig-abc" }, + }); + }); + + it("echoes the signature back on the next turn", async () => { + providerFetch.mockResolvedValue(streamingResponse([])); + + await collect( + geminiAdapter.streamChat( + { providerType: "gemini", apiKey: "k" }, + { + ...REQUEST, + messages: [ + { role: "user", content: "hi" }, + { + role: "assistant", + content: "", + toolCalls: [ + { + id: "c1", + name: "list_hosts", + arguments: {}, + providerSignature: "sig-abc", + }, + ], + }, + { + role: "tool", + content: "{}", + toolCallId: "c1", + toolName: "list_hosts", + }, + ], + }, + ), + ); + + const body = JSON.parse(providerFetch.mock.calls[0][1].body as string); + const modelTurn = body.contents.find((c: any) => c.role === "model"); + expect(modelTurn.parts[0].thoughtSignature).toBe("sig-abc"); + }); +}); + +describe("assertOk error messages", () => { + beforeEach(() => vi.clearAllMocks()); + + function errorResponse(status: number, body: string): Response { + return { + ok: false, + status, + text: async () => body, + } as unknown as Response; + } + + it("pulls the message out of a nested error body", async () => { + // Slicing the raw JSON used to cut the text off mid-sentence. + providerFetch.mockResolvedValue( + errorResponse( + 400, + JSON.stringify({ + error: { + code: 400, + message: "Function call is missing a signature.", + }, + }), + ), + ); + + await expect( + collect(openAiAdapter.streamChat({ providerType: "openai" }, REQUEST)), + ).rejects.toThrow("Function call is missing a signature."); + }); + + it("explains a rate limit instead of dumping the body", async () => { + providerFetch.mockResolvedValue( + errorResponse( + 429, + JSON.stringify({ error: { message: "You exceeded your quota." } }), + ), + ); + + await expect( + collect(openAiAdapter.streamChat({ providerType: "openai" }, REQUEST)), + ).rejects.toThrow(/rate limit reached/i); + }); + + it("explains a rejected key", async () => { + providerFetch.mockResolvedValue( + errorResponse(401, JSON.stringify({ error: { message: "Bad key" } })), + ); + + await expect( + collect(openAiAdapter.streamChat({ providerType: "openai" }, REQUEST)), + ).rejects.toThrow(/rejected the API key/i); + }); + + it("falls back to a trimmed snippet for a non-JSON body", async () => { + providerFetch.mockResolvedValue(errorResponse(500, "upstream exploded")); + + await expect( + collect(openAiAdapter.streamChat({ providerType: "openai" }, REQUEST)), + ).rejects.toThrow("upstream exploded"); + }); +}); diff --git a/src/backend/tests/ai/redaction.test.ts b/src/backend/tests/ai/redaction.test.ts new file mode 100644 index 0000000..b00a05e --- /dev/null +++ b/src/backend/tests/ai/redaction.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { REDACTED, redact, redactString } from "../../ai/redaction.js"; + +describe("redact", () => { + it("drops secret-named fields at any depth", () => { + const input = { + name: "web-1", + password: "hunter2", + nested: { privateKey: "abc", apiKey: "def", port: 22 }, + list: [{ keyPassword: "xyz", label: "ok" }], + }; + + const output = redact(input) as any; + + expect(output.name).toBe("web-1"); + expect(output.password).toBe(REDACTED); + expect(output.nested.privateKey).toBe(REDACTED); + expect(output.nested.apiKey).toBe(REDACTED); + expect(output.nested.port).toBe(22); + expect(output.list[0].keyPassword).toBe(REDACTED); + expect(output.list[0].label).toBe("ok"); + }); + + it("keeps a null secret null so absence stays distinguishable", () => { + const output = redact({ password: null }) as any; + expect(output.password).toBeNull(); + }); + + it("leaves ordinary values untouched", () => { + const input = { id: 4, enabled: true, tags: ["a", "b"], note: null }; + expect(redact(input)).toEqual(input); + }); + + it("does not recurse forever on a cyclic object", () => { + const cyclic: Record = { name: "loop" }; + cyclic.self = cyclic; + expect(() => redact(cyclic)).not.toThrow(); + }); +}); + +describe("redactString", () => { + it("masks private key blocks", () => { + const text = + "-----BEGIN OPENSSH PRIVATE KEY-----\nabc123\n-----END OPENSSH PRIVATE KEY-----"; + expect(redactString(text)).toBe("[redacted private key]"); + }); + + it("masks provider api keys", () => { + expect(redactString("key is sk-abcdefghijklmnopqrst here")).toContain( + "[redacted api key]", + ); + expect(redactString("key is sk-ant-abcdefghijklmnopqrst here")).toContain( + "[redacted api key]", + ); + }); + + it("masks bearer tokens and jwts", () => { + expect( + redactString("Authorization: Bearer abcdefghijklmnopqrst"), + ).toContain("Bearer [redacted]"); + expect( + redactString("token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abcdefgh"), + ).toContain("[redacted token]"); + }); + + it("masks Termix api keys", () => { + expect(redactString("tmx_abcdefghijklmnopqrstuvwx")).toContain( + "[redacted token]", + ); + }); + + it("leaves ordinary prose alone", () => { + const text = "The disk on web-1 is 82 percent full."; + expect(redactString(text)).toBe(text); + }); +}); diff --git a/src/backend/tests/ai/tool-catalog.test.ts b/src/backend/tests/ai/tool-catalog.test.ts new file mode 100644 index 0000000..898302d --- /dev/null +++ b/src/backend/tests/ai/tool-catalog.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest"; +import { + AI_TOOLS, + FORBIDDEN_DOMAINS, + getTool, + listToolNames, + toolDefinitions, +} from "../../ai/tools/catalog.js"; + +/** + * The security regression test for the whole feature. + * + * PermissionManager.requirePermission is defined but mounted on zero routes, + * so RBAC strings do not gate anything at the route layer. "The assistant + * cannot reach credentials or user administration" is therefore a property of + * this catalog, and nothing else. If a future change adds a tool that touches a + * forbidden domain, this test is what catches it. + */ +describe("AI tool catalog", () => { + it("exposes no tool naming a forbidden domain", () => { + for (const tool of AI_TOOLS) { + for (const domain of FORBIDDEN_DOMAINS) { + expect( + tool.name.includes(domain), + `${tool.name} references the forbidden domain "${domain}"`, + ).toBe(false); + } + } + }); + + it("has no tool that could read a credential", () => { + const banned = [ + "get_credential", + "list_credentials", + "get_password", + "get_private_key", + "get_api_key", + "create_user", + "delete_user", + "grant_permission", + "update_settings", + ]; + for (const name of banned) { + expect(getTool(name), `${name} must not exist`).toBeUndefined(); + } + }); + + it("only allows read or propose categories", () => { + for (const tool of AI_TOOLS) { + expect(["read", "propose"]).toContain(tool.category); + } + }); + + it("names every tool by its category", () => { + // A propose tool that does not say "propose" would read as a direct action + // in the transcript, which is exactly the confusion this feature avoids. + for (const tool of AI_TOOLS) { + if (tool.category === "propose") { + expect( + tool.name.startsWith("propose_"), + `${tool.name} is a propose tool but is not named propose_*`, + ).toBe(true); + } else { + expect( + tool.name.startsWith("propose_"), + `${tool.name} is a read tool but is named propose_*`, + ).toBe(false); + } + } + }); + + it("has unique tool names", () => { + const names = listToolNames(); + expect(new Set(names).size).toBe(names.length); + }); + + it("gives every tool a described object schema", () => { + for (const definition of toolDefinitions()) { + expect(definition.description.length, definition.name).toBeGreaterThan( + 20, + ); + expect(definition.parameters.type, definition.name).toBe("object"); + // additionalProperties:false keeps a model from smuggling extra fields + // past the handler's explicit reads. + expect(definition.parameters.additionalProperties, definition.name).toBe( + false, + ); + } + }); + + it("never takes a userId from the model", () => { + // Ownership is always derived from the verified JWT. A userId parameter + // would let the model ask for another account's data. + for (const tool of AI_TOOLS) { + const properties = (tool.parameters.properties ?? {}) as Record< + string, + unknown + >; + for (const key of Object.keys(properties)) { + expect( + /^user_?id$/i.test(key), + `${tool.name} accepts a model-supplied ${key}`, + ).toBe(false); + } + } + }); + + it("still offers the read tools the assistant needs to be useful", () => { + for (const name of ["list_hosts", "list_snippets", "list_automations"]) { + expect(getTool(name), name).toBeDefined(); + } + }); +}); diff --git a/src/backend/tests/automations/conditions.test.ts b/src/backend/tests/automations/conditions.test.ts new file mode 100644 index 0000000..a054f35 --- /dev/null +++ b/src/backend/tests/automations/conditions.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, it } from "vitest"; +import { + compare, + extractMetricValue, + hasDwelled, + isCoolingDown, + metricStateKey, + severityForValue, + type MetricsSnapshot, +} from "../../automations/conditions.js"; + +const metrics: MetricsSnapshot = { + cpu: { percent: 42.5, load: [1.5, 1.2, 0.9] }, + memory: { percent: 61, usedGiB: 7.5 }, + disk: { + percent: 30, + filesystems: [ + { mount: "/", percent: 30, availableBytes: 100 }, + { mount: "/data", percent: 93.4, availableBytes: 25 }, + ], + }, + network: { + interfaces: [ + { name: "eth0", rxBytes: "1000", txBytes: "2000" }, + { name: "eth1", rxBytes: "50", txBytes: "60" }, + ], + }, + temperature: { highestCelsius: 71 }, + uptime: { seconds: 86400 }, + processes: { total: 210 }, +}; + +describe("extractMetricValue", () => { + it("reads simple scalar paths", () => { + expect(extractMetricValue(metrics, { path: "cpu.percent" })).toBe(42.5); + expect(extractMetricValue(metrics, { path: "memory.percent" })).toBe(61); + expect( + extractMetricValue(metrics, { path: "temperature.highestCelsius" }), + ).toBe(71); + expect(extractMetricValue(metrics, { path: "uptime.seconds" })).toBe(86400); + expect(extractMetricValue(metrics, { path: "processes.total" })).toBe(210); + }); + + it("reads load averages positionally", () => { + expect(extractMetricValue(metrics, { path: "cpu.load1" })).toBe(1.5); + expect(extractMetricValue(metrics, { path: "cpu.load15" })).toBe(0.9); + }); + + it("reads a specific mount rather than the aggregate", () => { + // The motivating case: /data is nearly full while / is fine. + expect( + extractMetricValue(metrics, { path: "disk.percent", mount: "/data" }), + ).toBe(93.4); + expect(extractMetricValue(metrics, { path: "disk.percent" })).toBe(30); + }); + + it("returns null for a mount that is not present", () => { + expect( + extractMetricValue(metrics, { path: "disk.percent", mount: "/nope" }), + ).toBeNull(); + }); + + it("selects a named interface and coerces string counters", () => { + expect( + extractMetricValue(metrics, { path: "network.rxBytes", iface: "eth1" }), + ).toBe(50); + expect(extractMetricValue(metrics, { path: "network.rxBytes" })).toBe(1000); + }); + + it("extracts per-interface network rates for bandwidth alerts", () => { + const rateMetrics = { + network: { + interfaces: [ + { name: "eth0", rxRateBps: 1024, txRateBps: 2048 }, + { name: "eth1", rxRateBps: 4096, txRateBps: 8192 }, + ], + }, + }; + + expect( + extractMetricValue(rateMetrics, { + path: "network.rxRateBps", + iface: "eth1", + }), + ).toBe(4096); + expect( + extractMetricValue(rateMetrics, { + path: "network.txRateBps", + iface: "eth0", + }), + ).toBe(2048); + }); + + it("returns null for missing metrics rather than throwing", () => { + expect(extractMetricValue(null, { path: "cpu.percent" })).toBeNull(); + expect(extractMetricValue({}, { path: "cpu.percent" })).toBeNull(); + expect( + extractMetricValue({ cpu: { percent: null } }, { path: "cpu.percent" }), + ).toBeNull(); + }); +}); + +describe("metricStateKey", () => { + it("scopes state per mount so dwell tracks one filesystem", () => { + expect(metricStateKey(7, { path: "disk.percent" })).toBe("7"); + expect(metricStateKey(7, { path: "disk.percent", mount: "/data" })).toBe( + "7:/data", + ); + expect(metricStateKey(7, { path: "network.rxBytes", iface: "eth1" })).toBe( + "7:eth1", + ); + }); +}); + +describe("compare", () => { + it("handles numeric operators", () => { + expect(compare(93, ">", 90)).toBe(true); + expect(compare(90, ">", 90)).toBe(false); + expect(compare(90, ">=", 90)).toBe(true); + expect(compare(10, "<", 90)).toBe(true); + expect(compare(90, "<=", 90)).toBe(true); + }); + + it("compares numeric strings numerically", () => { + expect(compare("93", ">", "90")).toBe(true); + // Lexically "9" > "10", so this would be wrong as a string compare. + expect(compare("9", "<", "10")).toBe(true); + }); + + it("falls back to string equality for non-numeric values", () => { + expect(compare("running", "==", "running")).toBe(true); + expect(compare("running", "!=", "exited")).toBe(true); + }); + + it("handles containment", () => { + expect(compare("disk full", "contains", "full")).toBe(true); + expect(compare("disk full", "not_contains", "full")).toBe(false); + expect(compare("all good", "not_contains", "error")).toBe(true); + }); + + it("treats changed as inequality of the rendered values", () => { + expect(compare("online", "changed", "offline")).toBe(true); + expect(compare("online", "changed", "online")).toBe(false); + }); + + it("is false when a numeric comparison has a non-numeric side", () => { + expect(compare("abc", ">", 5)).toBe(false); + }); +}); + +describe("isCoolingDown", () => { + const now = Date.parse("2026-01-01T12:00:00.000Z"); + + it("is false when nothing has fired yet", () => { + expect(isCoolingDown(null, 15, now)).toBe(false); + }); + + it("is true inside the window and false outside it", () => { + expect(isCoolingDown("2026-01-01T11:50:00.000Z", 15, now)).toBe(true); + expect(isCoolingDown("2026-01-01T11:40:00.000Z", 15, now)).toBe(false); + }); + + it("treats a zero cooldown as always ready", () => { + expect(isCoolingDown("2026-01-01T11:59:59.000Z", 0, now)).toBe(false); + }); +}); + +describe("hasDwelled", () => { + const now = Date.parse("2026-01-01T12:00:00.000Z"); + + it("fires immediately when no window is configured", () => { + expect(hasDwelled(null, undefined, now)).toBe(true); + expect(hasDwelled(null, 0, now)).toBe(true); + }); + + it("requires the window to have elapsed", () => { + expect(hasDwelled("2026-01-01T11:49:00.000Z", 600, now)).toBe(true); + expect(hasDwelled("2026-01-01T11:55:00.000Z", 600, now)).toBe(false); + }); + + it("is false when a window is set but no breach is open", () => { + expect(hasDwelled(null, 600, now)).toBe(false); + }); +}); + +describe("severityForValue", () => { + it("escalates at 95 and honours an explicit override", () => { + expect(severityForValue(96)).toBe("critical"); + expect(severityForValue(90)).toBe("warning"); + expect(severityForValue(96, "info")).toBe("info"); + }); +}); diff --git a/src/backend/tests/automations/cron.test.ts b/src/backend/tests/automations/cron.test.ts new file mode 100644 index 0000000..314c6bf --- /dev/null +++ b/src/backend/tests/automations/cron.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, it } from "vitest"; +import { + computeNextDueAt, + isValidCron, + isValidTimezone, + nextCronRun, + parseCron, +} from "../../automations/cron.js"; + +describe("parseCron", () => { + it("rejects anything that is not five fields", () => { + expect(() => parseCron("* * * *")).toThrow(/five fields/); + expect(() => parseCron("* * * * * *")).toThrow(/five fields/); + }); + + it("expands wildcards, lists, ranges and steps", () => { + const fields = parseCron("0,30 9-17 * * 1-5"); + expect([...fields.minutes]).toEqual([0, 30]); + expect([...fields.hours]).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]); + expect([...fields.daysOfWeek]).toEqual([1, 2, 3, 4, 5]); + expect(fields.dowRestricted).toBe(true); + expect(fields.domRestricted).toBe(false); + }); + + it("supports step syntax", () => { + expect([...parseCron("*/15 * * * *").minutes]).toEqual([0, 15, 30, 45]); + }); + + it("accepts month and day names", () => { + expect([...parseCron("0 0 1 jan *").months]).toEqual([1]); + expect([...parseCron("0 0 * * sun").daysOfWeek]).toEqual([0]); + }); + + it("treats day 7 as Sunday", () => { + expect([...parseCron("0 0 * * 7").daysOfWeek]).toEqual([0]); + }); + + it("rejects out of range values", () => { + expect(() => parseCron("60 * * * *")).toThrow(/out of range/); + expect(() => parseCron("* 24 * * *")).toThrow(/out of range/); + expect(() => parseCron("* * 0 * *")).toThrow(/out of range/); + }); + + it("reports validity without throwing", () => { + expect(isValidCron("*/5 * * * *")).toBe(true); + expect(isValidCron("nonsense")).toBe(false); + }); +}); + +describe("nextCronRun", () => { + it("finds the next matching minute", () => { + const from = new Date(2026, 0, 1, 10, 3, 30); + expect(nextCronRun("*/15 * * * *", from)).toEqual( + new Date(2026, 0, 1, 10, 15, 0, 0), + ); + }); + + it("never returns the starting minute", () => { + const from = new Date(2026, 0, 1, 10, 0, 0); + expect(nextCronRun("0 * * * *", from)).toEqual( + new Date(2026, 0, 1, 11, 0, 0, 0), + ); + }); + + it("rolls into the next day", () => { + const from = new Date(2026, 0, 1, 23, 45, 0); + expect(nextCronRun("0 2 * * *", from)).toEqual( + new Date(2026, 0, 2, 2, 0, 0, 0), + ); + }); + + it("unions day-of-month and day-of-week when both are set", () => { + // The 15th, or any Monday. + const from = new Date(2026, 0, 1, 0, 0, 0); + const next = nextCronRun("0 0 15 * 1", from); + expect(next).not.toBeNull(); + const isFifteenth = next!.getDate() === 15; + const isMonday = next!.getDay() === 1; + expect(isFifteenth || isMonday).toBe(true); + }); + + it("gives up on a date that can never match", () => { + expect(nextCronRun("0 0 30 2 *", new Date(2026, 0, 1))).toBeNull(); + }); +}); + +describe("computeNextDueAt", () => { + it("prefers an interval over a cron expression", () => { + const from = new Date("2026-01-01T00:00:00.000Z"); + expect( + computeNextDueAt({ intervalSeconds: 300, cron: "0 0 * * *" }, from), + ).toBe("2026-01-01T00:05:00.000Z"); + }); + + it("falls back to cron", () => { + const from = new Date(2026, 0, 1, 10, 0, 0); + const due = computeNextDueAt({ cron: "30 10 * * *" }, from); + expect(due).toBe(new Date(2026, 0, 1, 10, 30, 0, 0).toISOString()); + }); + + it("returns null when nothing is scheduled", () => { + expect(computeNextDueAt({})).toBeNull(); + }); + + it("passes the zone through to the cron evaluation", () => { + // 02:00 in Tokyo on 2026-06-02 is 17:00 UTC on 2026-06-01. + const from = new Date("2026-06-01T00:00:00.000Z"); + expect( + computeNextDueAt({ cron: "0 2 * * *", timezone: "Asia/Tokyo" }, from), + ).toBe("2026-06-01T17:00:00.000Z"); + }); + + it("ignores the zone for interval schedules", () => { + const from = new Date("2026-01-01T00:00:00.000Z"); + expect( + computeNextDueAt({ intervalSeconds: 600, timezone: "Asia/Tokyo" }, from), + ).toBe("2026-01-01T00:10:00.000Z"); + }); +}); + +describe("time zone handling", () => { + it("resolves a daily cron against the given zone", () => { + const from = new Date("2026-06-01T00:00:00.000Z"); + // 09:30 New York in June (UTC-4) is 13:30 UTC. + const next = nextCronRun("30 9 * * *", from, "America/New_York"); + expect(next?.toISOString()).toBe("2026-06-01T13:30:00.000Z"); + }); + + it("tracks daylight saving, so the UTC instant shifts by an hour", () => { + const summer = nextCronRun( + "0 12 * * *", + new Date("2026-07-01T00:00:00.000Z"), + "America/New_York", + ); + const winter = nextCronRun( + "0 12 * * *", + new Date("2026-01-01T00:00:00.000Z"), + "America/New_York", + ); + + // Noon local both times, but UTC-4 in July and UTC-5 in January. + expect(summer?.toISOString()).toBe("2026-07-01T16:00:00.000Z"); + expect(winter?.toISOString()).toBe("2026-01-01T17:00:00.000Z"); + }); + + it("matches the day of week in the target zone, not the server's", () => { + // 23:00 UTC Sunday is already Monday in Tokyo. + const next = nextCronRun( + "0 8 * * mon", + new Date("2026-06-07T22:00:00.000Z"), + "Asia/Tokyo", + ); + expect(next?.toISOString()).toBe("2026-06-07T23:00:00.000Z"); + }); + + it("falls back to server time for an unknown zone instead of throwing", () => { + const from = new Date(2026, 0, 1, 10, 0, 0); + const next = nextCronRun("30 10 * * *", from, "Not/AZone"); + expect(next).toEqual(new Date(2026, 0, 1, 10, 30, 0, 0)); + }); + + it("handles midnight, which some hour cycles format as 24", () => { + const next = nextCronRun( + "0 0 * * *", + new Date("2026-06-01T10:00:00.000Z"), + "UTC", + ); + expect(next?.toISOString()).toBe("2026-06-02T00:00:00.000Z"); + }); +}); + +describe("isValidTimezone", () => { + it("accepts real zones", () => { + expect(isValidTimezone("UTC")).toBe(true); + expect(isValidTimezone("Europe/London")).toBe(true); + }); + + it("rejects made-up ones", () => { + expect(isValidTimezone("Middle/Earth")).toBe(false); + expect(isValidTimezone("")).toBe(false); + }); +}); diff --git a/src/backend/tests/automations/docker-watcher.test.ts b/src/backend/tests/automations/docker-watcher.test.ts new file mode 100644 index 0000000..6cfb158 --- /dev/null +++ b/src/backend/tests/automations/docker-watcher.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "vitest"; +import { + diffContainerStates, + parseContainerStates, +} from "../../automations/docker-watcher.js"; + +/** + * The polling side needs SSH, so what is tested here is the pure part: turning + * `docker ps` output into states, and turning two snapshots into events. + */ + +describe("parseContainerStates", () => { + it("reads name, state and health out of the ps output", () => { + const output = [ + '{"name":"web","state":"running","status":"Up 2 hours"}', + '{"name":"db","state":"exited","status":"Exited (0) 5 minutes ago"}', + '{"name":"api","state":"running","status":"Up 1 hour (unhealthy)"}', + ].join("\n"); + + const states = parseContainerStates(output); + + expect(states.get("web")).toEqual({ state: "running", unhealthy: false }); + expect(states.get("db")).toEqual({ state: "exited", unhealthy: false }); + expect(states.get("api")).toEqual({ state: "running", unhealthy: true }); + }); + + it("skips blank and malformed lines rather than failing the poll", () => { + const output = [ + '{"name":"web","state":"running","status":"Up"}', + "", + "not json at all", + '{"state":"running"}', + ].join("\n"); + + const states = parseContainerStates(output); + expect([...states.keys()]).toEqual(["web"]); + }); + + it("returns nothing for empty output", () => { + expect(parseContainerStates("").size).toBe(0); + }); +}); + +describe("diffContainerStates", () => { + const running = { state: "running", unhealthy: false }; + const exited = { state: "exited", unhealthy: false }; + + it("reports a container that stopped", () => { + const events = diffContainerStates( + new Map([["web", running]]), + new Map([["web", exited]]), + ); + expect(events).toEqual([{ container: "web", event: "exited" }]); + }); + + it("reports a container that started", () => { + const events = diffContainerStates( + new Map([["web", exited]]), + new Map([["web", running]]), + ); + expect(events).toEqual([{ container: "web", event: "started" }]); + }); + + it("reports a container that began restarting", () => { + const events = diffContainerStates( + new Map([["web", running]]), + new Map([["web", { state: "restarting", unhealthy: false }]]), + ); + expect(events).toEqual([{ container: "web", event: "restarting" }]); + }); + + it("reports a container that went unhealthy while still running", () => { + const events = diffContainerStates( + new Map([["web", running]]), + new Map([["web", { state: "running", unhealthy: true }]]), + ); + expect(events).toEqual([{ container: "web", event: "unhealthy" }]); + }); + + it("stays quiet when nothing changed", () => { + const events = diffContainerStates( + new Map([["web", running]]), + new Map([["web", running]]), + ); + expect(events).toEqual([]); + }); + + it("does not re-announce a container that is still unhealthy", () => { + const unhealthy = { state: "running", unhealthy: true }; + const events = diffContainerStates( + new Map([["web", unhealthy]]), + new Map([["web", unhealthy]]), + ); + expect(events).toEqual([]); + }); + + // A first sighting is a baseline, not an event: otherwise every container + // running at boot would report itself as freshly started. + it("treats a newly seen container as a baseline", () => { + const events = diffContainerStates(new Map(), new Map([["web", running]])); + expect(events).toEqual([]); + }); + + it("ignores a container that disappeared", () => { + const events = diffContainerStates(new Map([["web", running]]), new Map()); + expect(events).toEqual([]); + }); +}); diff --git a/src/backend/tests/automations/engine.test.ts b/src/backend/tests/automations/engine.test.ts new file mode 100644 index 0000000..c4dd3f0 --- /dev/null +++ b/src/backend/tests/automations/engine.test.ts @@ -0,0 +1,567 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { AutomationDefinition, Step } from "../../../types/automations.js"; + +/** + * The engine reaches the database through the repository factory and the + * outside world through the step executors, so both are mocked here. What is + * under test is the run loop itself: ordering, branching, error policy, + * concurrency, recursion and dry-run. + */ + +interface FakeAutomation { + id: number; + userId: string; + name: string; + enabled: boolean; + definition: string; + concurrencyPolicy: string; + maxRunSeconds: number; + dryRun: boolean; +} + +const automations = new Map(); +const runs: Array> = []; +const runSteps: Array> = []; +let nextRunId = 1; +let nextStepRowId = 1; + +const repository = { + findById: vi.fn(async (id: number) => automations.get(id) ?? null), + createRun: vi.fn(async (input: Record) => { + const run = { id: nextRunId++, ...input }; + runs.push(run); + return run; + }), + finishRun: vi.fn(async (runId: number, input: Record) => { + const run = runs.find((entry) => entry.id === runId); + if (run) Object.assign(run, input); + }), + createRunStep: vi.fn(async (input: Record) => { + const id = nextStepRowId++; + runSteps.push({ id, ...input }); + return id; + }), + finishRunStep: vi.fn(async (id: number, input: Record) => { + const step = runSteps.find((entry) => entry.id === id); + if (step) Object.assign(step, input); + }), +}; + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentAutomationRepository: () => repository, +})); + +const executeStep = vi.fn(); +vi.mock("../../automations/actions/index.js", () => ({ + executeStep: (...args: unknown[]) => executeStep(...args), +})); + +const { AutomationEngine } = await import("../../automations/engine.js"); + +function defineAutomation( + steps: Step[], + overrides: Partial = {}, +): FakeAutomation { + const definition: AutomationDefinition = { + version: 1, + trigger: { kind: "webhook", tokenHash: "x" }, + steps, + }; + const automation: FakeAutomation = { + id: overrides.id ?? 1, + userId: "user-1", + name: "Test", + enabled: true, + definition: JSON.stringify(definition), + concurrencyPolicy: "skip", + maxRunSeconds: 300, + dryRun: false, + ...overrides, + }; + automations.set(automation.id, automation); + return automation; +} + +function step(partial: Partial & { id: string; type: string }): Step { + return partial as Step; +} + +beforeEach(() => { + automations.clear(); + runs.length = 0; + runSteps.length = 0; + nextRunId = 1; + nextStepRowId = 1; + vi.clearAllMocks(); + executeStep.mockResolvedValue({ success: true, output: "ok" }); + // The singleton carries in-flight state between tests. + (AutomationEngine as unknown as { instance?: unknown }).instance = undefined; +}); + +describe("AutomationEngine.run", () => { + it("runs steps in order and records each one", async () => { + defineAutomation([ + step({ id: "a", type: "run_command" }), + step({ id: "b", type: "http" }), + ]); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(outcome.status).toBe("success"); + expect(executeStep).toHaveBeenCalledTimes(2); + expect(runSteps.map((s) => s.stepId)).toEqual(["a", "b"]); + expect(runSteps.map((s) => s.stepIndex)).toEqual([0, 1]); + }); + + it("fails the run and stops when a step fails under the default policy", async () => { + defineAutomation([ + step({ id: "a", type: "run_command" }), + step({ id: "b", type: "http" }), + ]); + executeStep.mockResolvedValueOnce({ success: false, error: "boom" }); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(outcome.status).toBe("failed"); + expect(outcome.error).toBe("boom"); + expect(executeStep).toHaveBeenCalledTimes(1); + }); + + it("keeps going when a step is marked continue-on-error", async () => { + defineAutomation([ + step({ id: "a", type: "run_command", onError: "continue" }), + step({ id: "b", type: "http" }), + ]); + executeStep.mockResolvedValueOnce({ success: false, error: "boom" }); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(outcome.status).toBe("success"); + expect(executeStep).toHaveBeenCalledTimes(2); + }); + + it("skips disabled steps", async () => { + defineAutomation([ + step({ id: "a", type: "run_command", enabled: false }), + step({ id: "b", type: "http" }), + ]); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(executeStep).toHaveBeenCalledTimes(1); + expect(runSteps.map((s) => s.stepId)).toEqual(["b"]); + }); + + it("passes earlier step output to later steps", async () => { + defineAutomation([ + step({ id: "first", type: "run_command" }), + step({ id: "second", type: "http" }), + ]); + executeStep.mockResolvedValueOnce({ success: true, output: "hello" }); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + const secondCallContext = executeStep.mock.calls[1][1] as { + template: { steps: Record }; + }; + expect(secondCallContext.template.steps.first.stdout).toBe("hello"); + }); + + it("merges variables set by a step into the template context", async () => { + defineAutomation([ + step({ id: "setter", type: "set_var" }), + step({ id: "next", type: "http" }), + ]); + executeStep.mockResolvedValueOnce({ + success: true, + output: "x = 1", + vars: { x: "1" }, + }); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + const context = executeStep.mock.calls[1][1] as { + template: { vars: Record }; + }; + expect(context.template.vars.x).toBe("1"); + }); + + describe("if branching", () => { + it("runs the then branch when the condition matches", async () => { + defineAutomation([ + step({ + id: "cond", + type: "if", + condition: { left: "93", operator: ">", right: "90" }, + then: [step({ id: "yes", type: "http" })], + else: [step({ id: "no", type: "http" })], + }), + ]); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(executeStep).toHaveBeenCalledTimes(1); + expect(runSteps.map((s) => s.stepId)).toEqual(["cond", "yes"]); + }); + + it("runs the else branch when it does not", async () => { + defineAutomation([ + step({ + id: "cond", + type: "if", + condition: { left: "10", operator: ">", right: "90" }, + then: [step({ id: "yes", type: "http" })], + else: [step({ id: "no", type: "http" })], + }), + ]); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(runSteps.map((s) => s.stepId)).toEqual(["cond", "no"]); + }); + + it("resolves templates on both sides of the condition", async () => { + defineAutomation([ + step({ + id: "cond", + type: "if", + condition: { + left: "{{trigger.value}}", + operator: ">=", + right: "90", + }, + then: [step({ id: "yes", type: "http" })], + }), + ]); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "metric_threshold", + triggerContext: { value: 95 }, + }); + + expect(runSteps.map((s) => s.stepId)).toEqual(["cond", "yes"]); + }); + + it("treats an empty else as a no-op", async () => { + defineAutomation([ + step({ + id: "cond", + type: "if", + condition: { left: "1", operator: "==", right: "2" }, + then: [step({ id: "yes", type: "http" })], + }), + step({ id: "after", type: "http" }), + ]); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(runSteps.map((s) => s.stepId)).toEqual(["cond", "after"]); + }); + }); + + it("halts the run when a step returns a stop signal", async () => { + defineAutomation([ + step({ id: "a", type: "run_command" }), + step({ id: "b", type: "http" }), + ]); + executeStep.mockResolvedValueOnce({ + success: true, + halt: { status: "success" }, + }); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(outcome.status).toBe("success"); + expect(executeStep).toHaveBeenCalledTimes(1); + }); + + it("marks the run failed when a stop step asks for failure", async () => { + defineAutomation([step({ id: "a", type: "run_command" })]); + executeStep.mockResolvedValueOnce({ + success: true, + halt: { status: "failed" }, + }); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(outcome.status).toBe("failed"); + }); + + describe("recursion protection", () => { + it("refuses to re-enter an automation already in the chain", async () => { + defineAutomation([ + step({ id: "nested", type: "run_automation", automationId: 1 }), + ]); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + // The nested call is refused, and the refusal is recorded on the step. + const nestedStep = runSteps.find((s) => s.stepId === "nested"); + expect(nestedStep?.status).toBe("failed"); + expect(String(nestedStep?.error)).toMatch( + /already running in this chain/, + ); + expect(outcome.status).toBe("failed"); + }); + + it("refuses a mutual cycle between two automations", async () => { + defineAutomation( + [step({ id: "toB", type: "run_automation", automationId: 2 })], + { id: 1 }, + ); + defineAutomation( + [step({ id: "toA", type: "run_automation", automationId: 1 })], + { id: 2 }, + ); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + const inner = runSteps.find((s) => s.stepId === "toA"); + expect(inner?.status).toBe("failed"); + expect(String(inner?.error)).toMatch(/already running in this chain/); + }); + + it("refuses to nest deeper than the maximum depth", async () => { + defineAutomation([step({ id: "a", type: "http" })]); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + depth: 99, + }); + + expect(outcome.status).toBe("failed"); + expect(outcome.error).toMatch(/depth/); + expect(runs).toHaveLength(0); + }); + }); + + describe("concurrency", () => { + it("records a skipped run rather than dropping it silently", async () => { + defineAutomation([step({ id: "slow", type: "run_command" })], { + concurrencyPolicy: "skip", + }); + + let release: () => void = () => {}; + executeStep.mockImplementationOnce( + () => + new Promise((resolve) => { + release = () => resolve({ success: true, output: "done" }); + }), + ); + + const engine = AutomationEngine.getInstance(); + const first = engine.run({ automationId: 1, triggerType: "manual" }); + // Let the first run register itself as in flight. + await new Promise((resolve) => setTimeout(resolve, 10)); + + const second = await engine.run({ + automationId: 1, + triggerType: "manual", + }); + expect(second.status).toBe("skipped"); + + release(); + await first; + + const skipped = runs.find((run) => run.status === "skipped"); + expect(skipped).toBeDefined(); + expect(String(skipped?.error)).toMatch(/still in progress/); + }); + + it("skips the second of two triggers that arrive in the same tick", async () => { + defineAutomation([step({ id: "slow", type: "run_command" })], { + concurrencyPolicy: "skip", + }); + + executeStep.mockImplementation( + () => + new Promise((resolve) => + setTimeout(() => resolve({ success: true, output: "done" }), 20), + ), + ); + + // No gap between the two: the in-flight slot used to be claimed several + // awaits after it was checked, so both runs got through. + const engine = AutomationEngine.getInstance(); + const [first, second] = await Promise.all([ + engine.run({ automationId: 1, triggerType: "manual" }), + engine.run({ automationId: 1, triggerType: "manual" }), + ]); + + expect([first.status, second.status].sort()).toEqual([ + "skipped", + "success", + ]); + expect(executeStep).toHaveBeenCalledTimes(1); + }); + + it("releases the in-flight slot when the run row cannot be created", async () => { + defineAutomation([step({ id: "a", type: "run_command" })]); + repository.createRun.mockRejectedValueOnce(new Error("db down")); + + const engine = AutomationEngine.getInstance(); + const failed = await engine.run({ + automationId: 1, + triggerType: "manual", + }); + expect(failed.status).toBe("failed"); + + // A leaked slot would make every later run skip forever. + const after = await engine.run({ + automationId: 1, + triggerType: "manual", + }); + expect(after.status).toBe("success"); + }); + }); + + it("propagates the dry-run flag to executors", async () => { + defineAutomation([step({ id: "a", type: "http" })], { dryRun: true }); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + const context = executeStep.mock.calls[0][1] as { dryRun: boolean }; + expect(context.dryRun).toBe(true); + }); + + it("lets a caller force a dry run on a live automation", async () => { + defineAutomation([step({ id: "a", type: "http" })], { dryRun: false }); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + dryRun: true, + }); + + const context = executeStep.mock.calls[0][1] as { dryRun: boolean }; + expect(context.dryRun).toBe(true); + }); + + it("fails cleanly when the automation is missing", async () => { + const outcome = await AutomationEngine.getInstance().run({ + automationId: 404, + triggerType: "manual", + }); + expect(outcome).toMatchObject({ status: "failed", runId: null }); + }); + + it("fails cleanly when the definition is not valid JSON", async () => { + automations.set(1, { + id: 1, + userId: "user-1", + name: "Broken", + enabled: true, + definition: "not json", + concurrencyPolicy: "skip", + maxRunSeconds: 300, + dryRun: false, + }); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + expect(outcome.status).toBe("failed"); + expect(outcome.error).toMatch(/not valid JSON/); + }); + + it("turns a thrown executor error into a failed step", async () => { + defineAutomation([step({ id: "a", type: "run_command" })]); + executeStep.mockRejectedValueOnce(new Error("connection reset")); + + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(outcome.status).toBe("failed"); + expect(runSteps[0].status).toBe("failed"); + expect(String(runSteps[0].error)).toMatch(/connection reset/); + }); + + it("truncates very large step output", async () => { + defineAutomation([step({ id: "a", type: "run_command" })]); + executeStep.mockResolvedValueOnce({ + success: true, + output: "x".repeat(40_000), + }); + + await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(runSteps[0].truncated).toBe(true); + expect(String(runSteps[0].output)).toMatch(/truncated/); + }); + + it("stops once the run deadline has passed", async () => { + defineAutomation( + [step({ id: "a", type: "run_command" }), step({ id: "b", type: "http" })], + { maxRunSeconds: 1 }, + ); + + // The first step consumes the whole budget, so the second must not start. + executeStep.mockImplementationOnce(async () => { + vi.setSystemTime(Date.now() + 5_000); + return { success: true, output: "slow" }; + }); + + vi.useFakeTimers({ shouldAdvanceTime: true }); + try { + const outcome = await AutomationEngine.getInstance().run({ + automationId: 1, + triggerType: "manual", + }); + + expect(outcome.status).toBe("timeout"); + expect(executeStep).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/src/backend/tests/automations/headless-viewer.test.ts b/src/backend/tests/automations/headless-viewer.test.ts new file mode 100644 index 0000000..fbbcf9b --- /dev/null +++ b/src/backend/tests/automations/headless-viewer.test.ts @@ -0,0 +1,136 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const listAutomationWatchedHosts = vi.fn(); +vi.mock("../../automations/triggers.js", () => ({ + listAutomationWatchedHosts: () => listAutomationWatchedHosts(), +})); + +const { + automationSessionId, + reconcileHeadlessViewers, + releaseHeadlessViewers, + setViewerRegistry, +} = await import("../../automations/headless-viewer.js"); + +const registry = { + registerViewer: vi.fn(), + unregisterViewer: vi.fn(), + updateHeartbeat: vi.fn(() => true), +}; + +beforeEach(() => { + // Drop any viewers the previous test left behind before the mocks are + // cleared, so those unregister calls are not counted against this test. + releaseHeadlessViewers(); + vi.clearAllMocks(); + setViewerRegistry(registry); + listAutomationWatchedHosts.mockResolvedValue(new Map()); +}); + +describe("reconcileHeadlessViewers", () => { + it("registers a synthetic viewer for each watched host", async () => { + listAutomationWatchedHosts.mockResolvedValue( + new Map([ + [7, "user-1"], + [9, "user-2"], + ]), + ); + + const result = await reconcileHeadlessViewers(); + + expect(result.added).toBe(2); + expect(registry.registerViewer).toHaveBeenCalledWith( + 7, + "automation:7", + "user-1", + ); + expect(registry.registerViewer).toHaveBeenCalledWith( + 9, + "automation:9", + "user-2", + ); + }); + + it("heartbeats instead of re-registering a host it already holds", async () => { + listAutomationWatchedHosts.mockResolvedValue(new Map([[7, "user-1"]])); + await reconcileHeadlessViewers(); + registry.registerViewer.mockClear(); + + const second = await reconcileHeadlessViewers(); + + // The 120s reaper drops viewers with a stale heartbeat, so every tick has + // to refresh the ones it is keeping. + expect(registry.updateHeartbeat).toHaveBeenCalledWith("automation:7"); + expect(registry.registerViewer).not.toHaveBeenCalled(); + expect(second.added).toBe(0); + expect(second.active).toBe(1); + }); + + it("releases a viewer once no automation watches the host", async () => { + listAutomationWatchedHosts.mockResolvedValue(new Map([[7, "user-1"]])); + await reconcileHeadlessViewers(); + + listAutomationWatchedHosts.mockResolvedValue(new Map()); + const result = await reconcileHeadlessViewers(); + + expect(result.removed).toBe(1); + expect(result.active).toBe(0); + expect(registry.unregisterViewer).toHaveBeenCalledWith(7, "automation:7"); + }); + + it("does nothing when no registry has been wired up", async () => { + setViewerRegistry(null); + listAutomationWatchedHosts.mockResolvedValue(new Map([[7, "user-1"]])); + + const result = await reconcileHeadlessViewers(); + + expect(result).toEqual({ added: 0, removed: 0, active: 0 }); + expect(registry.registerViewer).not.toHaveBeenCalled(); + }); + + it("keeps going when the watch list cannot be loaded", async () => { + listAutomationWatchedHosts.mockRejectedValue(new Error("db down")); + await expect(reconcileHeadlessViewers()).resolves.toMatchObject({ + added: 0, + }); + }); + + it("survives a registry that throws on register", async () => { + listAutomationWatchedHosts.mockResolvedValue( + new Map([ + [7, "user-1"], + [8, "user-1"], + ]), + ); + registry.registerViewer.mockImplementationOnce(() => { + throw new Error("nope"); + }); + + const result = await reconcileHeadlessViewers(); + + // One host failing must not stop the other from being registered. + expect(result.added).toBe(1); + }); +}); + +describe("releaseHeadlessViewers", () => { + it("drops every viewer it is holding", async () => { + listAutomationWatchedHosts.mockResolvedValue( + new Map([ + [7, "user-1"], + [8, "user-1"], + ]), + ); + await reconcileHeadlessViewers(); + + releaseHeadlessViewers(); + + expect(registry.unregisterViewer).toHaveBeenCalledTimes(2); + }); +}); + +describe("automationSessionId", () => { + it("namespaces the session so it cannot collide with a real viewer", () => { + expect(automationSessionId(42)).toBe("automation:42"); + }); +}); diff --git a/src/backend/tests/automations/template.test.ts b/src/backend/tests/automations/template.test.ts new file mode 100644 index 0000000..969bfb8 --- /dev/null +++ b/src/backend/tests/automations/template.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from "vitest"; +import { + hasUnresolvedTokens, + redactSecrets, + renderRecord, + renderTemplate, +} from "../../automations/template.js"; + +const context = { + host: { id: 7, name: "Zeus", ip: "10.0.0.5", username: "root", port: 22 }, + trigger: { value: 93.4, mount: "/data" }, + steps: { check: { stdout: "ok\n", stderr: "", code: 0 } }, + vars: { target: "/var/log" }, +}; + +describe("renderTemplate", () => { + it("substitutes host, trigger, step and var tokens", () => { + expect(renderTemplate("{{host.name}} at {{trigger.value}}%", context)).toBe( + "Zeus at 93.4%", + ); + expect(renderTemplate("{{steps.check.stdout}}", context)).toBe("ok\n"); + expect(renderTemplate("{{vars.target}}", context)).toBe("/var/log"); + }); + + it("tolerates whitespace inside the braces", () => { + expect(renderTemplate("{{ host.name }}", context)).toBe("Zeus"); + }); + + it("leaves unknown tokens in place so typos are visible", () => { + expect(renderTemplate("{{host.nope}}", context)).toBe("{{host.nope}}"); + expect(hasUnresolvedTokens(renderTemplate("{{host.nope}}", context))).toBe( + true, + ); + }); + + it("returns the input untouched when there is nothing to render", () => { + expect(renderTemplate("plain text", context)).toBe("plain text"); + expect(renderTemplate("", context)).toBe(""); + }); + + it("does not escape or alter shell metacharacters", () => { + // Quoting is the caller's job; this keeps that boundary explicit. + const rendered = renderTemplate("{{vars.payload}}", { + vars: { payload: "; rm -rf /" }, + }); + expect(rendered).toBe("; rm -rf /"); + }); + + it("does not recursively expand a value that looks like a token", () => { + const rendered = renderTemplate("{{vars.a}}", { + vars: { a: "{{vars.b}}", b: "gotcha" }, + }); + expect(rendered).toBe("{{vars.b}}"); + }); + + it("serializes objects rather than printing [object Object]", () => { + expect(renderTemplate("{{trigger}}", { trigger: { a: 1 } })).toBe( + '{"a":1}', + ); + }); + + it("renders missing branches as the literal token", () => { + expect(renderTemplate("{{steps.other.stdout}}", context)).toBe( + "{{steps.other.stdout}}", + ); + }); +}); + +describe("renderRecord", () => { + it("renders values and leaves keys alone", () => { + expect(renderRecord({ "X-Host": "{{host.name}}" }, context)).toEqual({ + "X-Host": "Zeus", + }); + }); + + it("passes undefined through", () => { + expect(renderRecord(undefined, context)).toBeUndefined(); + }); +}); + +describe("redactSecrets", () => { + it("masks credential-shaped keys", () => { + expect( + redactSecrets({ + Authorization: "Bearer abc", + "X-Api-Key": "k", + token: "t", + password: "p", + Accept: "application/json", + }), + ).toEqual({ + Authorization: "***", + "X-Api-Key": "***", + token: "***", + password: "***", + Accept: "application/json", + }); + }); +}); diff --git a/src/backend/tests/automations/triggers.test.ts b/src/backend/tests/automations/triggers.test.ts new file mode 100644 index 0000000..e5beec0 --- /dev/null +++ b/src/backend/tests/automations/triggers.test.ts @@ -0,0 +1,444 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { + AutomationDefinition, + Trigger, +} from "../../../types/automations.js"; + +/** + * Trigger matching, dwell windows and cooldowns. The repository and the engine + * are mocked so these tests only exercise the decision of whether to fire. + */ + +interface FakeRow { + id: number; + userId: string; + name: string; + enabled: boolean; + definition: string; + concurrencyPolicy: string; + maxRunSeconds: number; + dryRun: boolean; +} + +const rows: FakeRow[] = []; +const triggerState = new Map>(); + +const repository = { + listEnabledForUser: vi.fn(async (userId: string) => + rows.filter((row) => row.userId === userId && row.enabled), + ), + listAllEnabled: vi.fn(async () => rows.filter((row) => row.enabled)), + getTriggerState: vi.fn(async (automationId: number, stateKey: string) => { + return triggerState.get(`${automationId}:${stateKey}`) ?? null; + }), + upsertTriggerState: vi.fn(async (input: Record) => { + const key = `${input.automationId}:${input.stateKey}`; + triggerState.set(key, { ...(triggerState.get(key) ?? {}), ...input }); + }), + clearBreach: vi.fn(async (automationId: number, stateKey: string) => { + const key = `${automationId}:${stateKey}`; + const existing = triggerState.get(key); + if (existing) existing.breachStartedAt = null; + }), +}; + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentAutomationRepository: () => repository, +})); + +const run = vi.fn(async () => ({ runId: 1, status: "success" as const })); +vi.mock("../../automations/engine.js", () => ({ + AutomationEngine: { getInstance: () => ({ run }) }, +})); + +const triggers = await import("../../automations/triggers.js"); + +function addAutomation(trigger: Trigger, overrides: Partial = {}) { + const definition: AutomationDefinition = { version: 1, trigger, steps: [] }; + const row: FakeRow = { + id: overrides.id ?? rows.length + 1, + userId: overrides.userId ?? "user-1", + name: "Test", + enabled: overrides.enabled ?? true, + definition: JSON.stringify(definition), + concurrencyPolicy: "skip", + maxRunSeconds: 300, + dryRun: false, + }; + rows.push(row); + return row; +} + +const diskMetrics = { + disk: { + percent: 30, + filesystems: [ + { mount: "/", percent: 30 }, + { mount: "/data", percent: 93 }, + ], + }, +}; + +beforeEach(() => { + rows.length = 0; + triggerState.clear(); + vi.clearAllMocks(); +}); + +describe("onMetrics", () => { + it("fires immediately when no dwell window is set", async () => { + addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 7 }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 90, + cooldownMinutes: 15, + }); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).toHaveBeenCalledTimes(1); + expect(run.mock.calls[0][0]).toMatchObject({ + triggerType: "metric_threshold", + triggerHostId: 7, + }); + }); + + it("watches the named mount rather than the aggregate", async () => { + // Root is at 30%, so a rule on / must not fire at a 90% threshold. + addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 7 }, + metric: { path: "disk.percent", mount: "/" }, + operator: ">", + value: 90, + cooldownMinutes: 15, + }); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).not.toHaveBeenCalled(); + }); + + it("opens a dwell window instead of firing on the first sample", async () => { + const row = addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 7 }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 90, + forSeconds: 600, + cooldownMinutes: 15, + }); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).not.toHaveBeenCalled(); + expect(triggerState.get(`${row.id}:7:/data`)).toMatchObject({ + breachStartedAt: expect.any(String), + }); + }); + + it("fires once the dwell window has elapsed", async () => { + const row = addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 7 }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 90, + forSeconds: 600, + cooldownMinutes: 15, + }); + triggerState.set(`${row.id}:7:/data`, { + breachStartedAt: new Date(Date.now() - 700_000).toISOString(), + }); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).toHaveBeenCalledTimes(1); + }); + + it("clears the window as soon as the value recovers", async () => { + const row = addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 7 }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 95, + forSeconds: 600, + cooldownMinutes: 15, + }); + triggerState.set(`${row.id}:7:/data`, { + breachStartedAt: new Date(Date.now() - 700_000).toISOString(), + }); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).not.toHaveBeenCalled(); + expect(repository.clearBreach).toHaveBeenCalled(); + }); + + it("stays quiet while the cooldown is open", async () => { + const row = addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 7 }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 90, + cooldownMinutes: 15, + }); + triggerState.set(`${row.id}:7:/data`, { + lastFiredAt: new Date(Date.now() - 60_000).toISOString(), + breachStartedAt: new Date(Date.now() - 700_000).toISOString(), + }); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).not.toHaveBeenCalled(); + }); + + it("ignores hosts outside the selector", async () => { + addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 99 }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 90, + cooldownMinutes: 15, + }); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).not.toHaveBeenCalled(); + }); + + it("never evaluates another user's automations", async () => { + addAutomation( + { + kind: "metric_threshold", + hostSelector: { kind: "all" }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 90, + cooldownMinutes: 15, + }, + { userId: "user-2" }, + ); + + await triggers.onMetrics({ + hostId: 7, + ownerUserId: "user-1", + metrics: diskMetrics, + }); + + expect(run).not.toHaveBeenCalled(); + }); +}); + +describe("onStatus", () => { + it("treats the first observation as a baseline", async () => { + addAutomation({ + kind: "host_status", + hostSelector: { kind: "host", hostId: 7 }, + to: "offline", + cooldownMinutes: 0, + }); + + await triggers.onStatus({ + hostId: 7, + ownerUserId: "user-1", + online: false, + }); + + // Otherwise every host would announce itself after a restart. + expect(run).not.toHaveBeenCalled(); + }); + + it("fires on a transition into the watched state", async () => { + const row = addAutomation({ + kind: "host_status", + hostSelector: { kind: "host", hostId: 7 }, + to: "offline", + cooldownMinutes: 0, + }); + triggerState.set(`${row.id}:7`, { lastObservedState: "online" }); + + await triggers.onStatus({ + hostId: 7, + ownerUserId: "user-1", + online: false, + }); + + expect(run).toHaveBeenCalledTimes(1); + }); + + it("does not fire on a transition in the other direction", async () => { + const row = addAutomation({ + kind: "host_status", + hostSelector: { kind: "host", hostId: 7 }, + to: "offline", + cooldownMinutes: 0, + }); + triggerState.set(`${row.id}:7`, { lastObservedState: "offline" }); + + await triggers.onStatus({ hostId: 7, ownerUserId: "user-1", online: true }); + + expect(run).not.toHaveBeenCalled(); + }); + + it("stays quiet while the state is unchanged", async () => { + const row = addAutomation({ + kind: "host_status", + hostSelector: { kind: "host", hostId: 7 }, + to: "offline", + cooldownMinutes: 0, + }); + triggerState.set(`${row.id}:7`, { lastObservedState: "offline" }); + + await triggers.onStatus({ + hostId: 7, + ownerUserId: "user-1", + online: false, + }); + + expect(run).not.toHaveBeenCalled(); + }); +}); + +describe("onHealthCheck", () => { + it("fires when a check transitions to failing", async () => { + const row = addAutomation({ + kind: "health_check", + hostSelector: { kind: "host", hostId: 7 }, + to: "failing", + cooldownMinutes: 0, + }); + triggerState.set(`${row.id}:7:web`, { lastObservedState: "recovered" }); + + await triggers.onHealthCheck({ + hostId: 7, + userId: "user-1", + checkId: "web", + ok: false, + }); + + expect(run).toHaveBeenCalledTimes(1); + }); + + it("ignores a different check id", async () => { + const row = addAutomation({ + kind: "health_check", + hostSelector: { kind: "host", hostId: 7 }, + checkId: "db", + to: "failing", + cooldownMinutes: 0, + }); + triggerState.set(`${row.id}:7:web`, { lastObservedState: "recovered" }); + + await triggers.onHealthCheck({ + hostId: 7, + userId: "user-1", + checkId: "web", + ok: false, + }); + + expect(run).not.toHaveBeenCalled(); + }); +}); + +describe("onDockerEvent", () => { + it("fires for a matching container and event", async () => { + addAutomation({ + kind: "docker_event", + hostSelector: { kind: "host", hostId: 7 }, + container: "api", + event: "exited", + cooldownMinutes: 0, + }); + + await triggers.onDockerEvent({ + hostId: 7, + ownerUserId: "user-1", + container: "api", + event: "exited", + }); + + expect(run).toHaveBeenCalledTimes(1); + }); + + it("ignores a different container", async () => { + addAutomation({ + kind: "docker_event", + hostSelector: { kind: "host", hostId: 7 }, + container: "api", + event: "exited", + cooldownMinutes: 0, + }); + + await triggers.onDockerEvent({ + hostId: 7, + ownerUserId: "user-1", + container: "worker", + event: "exited", + }); + + expect(run).not.toHaveBeenCalled(); + }); +}); + +describe("listAutomationWatchedHosts", () => { + it("collects hosts from metric triggers so they can be polled headlessly", async () => { + addAutomation({ + kind: "metric_threshold", + hostSelector: { kind: "hosts", hostIds: [3, 4] }, + metric: { path: "cpu.percent" }, + operator: ">", + value: 90, + cooldownMinutes: 15, + }); + + const watched = await triggers.listAutomationWatchedHosts(); + + expect([...watched.keys()].sort()).toEqual([3, 4]); + }); + + it("ignores triggers that do not need heavy collection", async () => { + addAutomation({ + kind: "host_status", + hostSelector: { kind: "host", hostId: 3 }, + to: "offline", + cooldownMinutes: 0, + }); + + expect((await triggers.listAutomationWatchedHosts()).size).toBe(0); + }); +}); diff --git a/src/backend/tests/database/db/audit-log-user-id-nullable.test.ts b/src/backend/tests/database/db/audit-log-user-id-nullable.test.ts new file mode 100644 index 0000000..73fdc86 --- /dev/null +++ b/src/backend/tests/database/db/audit-log-user-id-nullable.test.ts @@ -0,0 +1,114 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * The audit trail outlives the account it belongs to: deleting a user + * anonymises their entries by nulling `user_id` and leaving `username` behind. + * + * The Drizzle schema said so, the repository was written against it, but the + * runtime bootstrap still created `user_id TEXT NOT NULL`. A second, corrected + * `CREATE TABLE IF NOT EXISTS` further down was a no-op โ€” the table already + * existed โ€” so every fresh install got the old constraint and every user + * deletion (including the OIDC account-link cleanup) failed once the account + * had logged in at least once. + */ +describe("audit_logs.user_id is nullable", () => { + let dataDir: string; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-audit-schema-")); + vi.resetModules(); + process.env.DATA_DIR = dataDir; + process.env.DB_FILE_ENCRYPTION = "false"; + process.env.ALLOW_EMPTY_DATA_DIR = "true"; + }); + + afterEach(() => { + delete process.env.DATA_DIR; + delete process.env.DB_FILE_ENCRYPTION; + delete process.env.ALLOW_EMPTY_DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + function userIdIsNotNull(sqlite: Database.Database): boolean { + const columns = sqlite.prepare("PRAGMA table_info(audit_logs)").all() as Array<{ + name: string; + notnull: number; + }>; + return columns.find((col) => col.name === "user_id")?.notnull === 1; + } + + it("lets a fresh install outlive the account it logged", async () => { + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + const sqlite = db.getSqlite(); + + expect(userIdIsNotNull(sqlite)).toBe(false); + + sqlite + .prepare("INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)") + .run("user-1", "alice", "hash"); + sqlite + .prepare( + `INSERT INTO audit_logs (user_id, username, action, resource_type, success) + VALUES (?, ?, ?, ?, ?)`, + ) + .run("user-1", "alice", "login", "auth", 1); + + expect(() => sqlite.prepare("DELETE FROM users WHERE id = ?").run("user-1")).not.toThrow(); + + const row = sqlite.prepare("SELECT user_id, username FROM audit_logs").get() as { + user_id: string | null; + username: string; + }; + + // The reference goes, the attribution stays. + expect(row.user_id).toBeNull(); + expect(row.username).toBe("alice"); + }); + + it("rebuilds an existing table that still has the constraint, keeping its rows", async () => { + const seed = new Database(":memory:"); + seed.exec(` + CREATE TABLE audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + resource_name TEXT, + details TEXT, + ip_address TEXT, + user_agent TEXT, + success INTEGER NOT NULL, + error_message TEXT, + timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + seed + .prepare( + `INSERT INTO audit_logs (user_id, username, action, resource_type, success) + VALUES (?, ?, ?, ?, ?)`, + ) + .run("user-1", "alice", "login", "auth", 1); + fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize()); + seed.close(); + + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + const sqlite = db.getSqlite(); + + expect(userIdIsNotNull(sqlite)).toBe(false); + + const row = sqlite.prepare("SELECT user_id, username FROM audit_logs").get() as { + user_id: string | null; + username: string; + }; + expect(row.user_id).toBe("user-1"); + expect(row.username).toBe("alice"); + }); +}); diff --git a/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts b/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts new file mode 100644 index 0000000..ffd7d76 --- /dev/null +++ b/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts @@ -0,0 +1,124 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * `migrateSchema()` carried a `SELECT id FROM LIMIT 1` probe for a + * number of tables that the primary bootstrap already creates. The probe never + * threw, so the `CREATE TABLE IF NOT EXISTS` in its catch never ran โ€” and two + * of those unreachable copies had drifted away from the real definition + * (`sessions` had lost `ON DELETE CASCADE`; `session_recordings` still had the + * pre-#1128 `user_id NOT NULL` with `ON DELETE CASCADE` and no `username`). + * + * They are gone now. What has to stay true is that the bootstrap alone + * produces every one of those tables, from an empty database and from a + * database that predates them. + */ +describe("bootstrap creates the tables the removed probes covered", () => { + let dataDir: string; + + // Exactly the tables whose unreachable re-creation was deleted. + const TABLES = [ + "c2s_tunnel_presets", + "sessions", + "trusted_devices", + "host_access", + "roles", + "user_roles", + "audit_logs", + "session_recordings", + "api_keys", + "session_shares", + "session_share_participants", + ]; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-bootstrap-")); + vi.resetModules(); + process.env.DATA_DIR = dataDir; + process.env.DB_FILE_ENCRYPTION = "false"; + process.env.ALLOW_EMPTY_DATA_DIR = "true"; + }); + + afterEach(() => { + delete process.env.DATA_DIR; + delete process.env.DB_FILE_ENCRYPTION; + delete process.env.ALLOW_EMPTY_DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + function tablesIn(sqlite: Database.Database): Set { + const rows = sqlite + .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") + .pluck() + .all() as string[]; + return new Set(rows); + } + + it("creates them all on a fresh database", async () => { + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + + const present = tablesIn(db.getSqlite()); + expect([...TABLES].filter((t) => !present.has(t))).toEqual([]); + }); + + it("creates them on a database old enough to predate them", async () => { + // A database with users and hosts but none of the tables above โ€” the + // upgrade path the deleted probes appeared to be protecting. + const seed = new Database(":memory:"); + seed.exec(` + CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password_hash TEXT NOT NULL + ); + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'); + `); + fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize()); + seed.close(); + + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + const sqlite = db.getSqlite(); + + const present = tablesIn(sqlite); + expect([...TABLES].filter((t) => !present.has(t))).toEqual([]); + + // The row that was already there is still there: this is an upgrade, not + // a rebuild. + const count = sqlite + .prepare("SELECT COUNT(*) FROM users") + .pluck() + .get() as number; + expect(count).toBe(1); + }); + + it("keeps the definitions the stale copies disagreed with", async () => { + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + const sqlite = db.getSqlite(); + + // session_recordings: nullable user_id with the attribution kept, per + // "audit trails survive the account" โ€” not the NOT NULL + CASCADE the + // dead copy still carried. + const columns = sqlite + .prepare("PRAGMA table_info(session_recordings)") + .all() as Array<{ name: string; notnull: number }>; + const userId = columns.find((c) => c.name === "user_id"); + expect(userId?.notnull).toBe(0); + expect(columns.some((c) => c.name === "username")).toBe(true); + + // sessions: the dead copy had dropped ON DELETE CASCADE. + const sql = sqlite + .prepare( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'sessions'", + ) + .pluck() + .get() as string; + expect(sql.replace(/\s+/g, " ")).toContain("ON DELETE CASCADE"); + }); +}); diff --git a/src/backend/tests/database/db/bootstrap-matches-schema.test.ts b/src/backend/tests/database/db/bootstrap-matches-schema.test.ts new file mode 100644 index 0000000..e2f4d26 --- /dev/null +++ b/src/backend/tests/database/db/bootstrap-matches-schema.test.ts @@ -0,0 +1,54 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { getTableName, is, Table } from "drizzle-orm"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as schema from "../../../database/db/schema.js"; + +/** + * SQLite tables are created by hand-written DDL in `db/index.ts`, not generated + * from the drizzle schema. Adding a table to `schema.ts` alone therefore + * type-checks, passes the repository tests (which build their fixture straight + * from the schema), and still fails at runtime with "no such table". + * + * That is exactly how the automations tables shipped broken, so this compares + * the two directly: every table drizzle knows about has to exist after boot. + */ +describe("bootstrap creates every table in the drizzle schema", () => { + let dataDir: string; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-schema-")); + vi.resetModules(); + process.env.DATA_DIR = dataDir; + process.env.DB_FILE_ENCRYPTION = "false"; + process.env.ALLOW_EMPTY_DATA_DIR = "true"; + }); + + afterEach(() => { + delete process.env.DATA_DIR; + delete process.env.DB_FILE_ENCRYPTION; + delete process.env.ALLOW_EMPTY_DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + it("leaves no schema table missing from a fresh database", async () => { + const expected = Object.values(schema) + .filter((value) => is(value, Table)) + .map((table) => getTableName(table as Table)) + .sort(); + + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + + const present = new Set( + db + .getSqlite() + .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") + .pluck() + .all() as string[], + ); + + expect(expected.filter((name) => !present.has(name))).toEqual([]); + }); +}); diff --git a/src/backend/tests/database/db/connect.test.ts b/src/backend/tests/database/db/connect.test.ts new file mode 100644 index 0000000..95e447e --- /dev/null +++ b/src/backend/tests/database/db/connect.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from "vitest"; +import { + assertUrlMatchesDialect, + connectRemoteDatabase, + databaseUrl, + poolMax, + sslOption, + DATABASE_POOL_MAX_ENV, + DATABASE_SSL_ENV, + DATABASE_URL_ENV, +} from "../../../database/db/connect.js"; + +describe("databaseUrl", () => { + it("is absent unless set", () => { + expect(databaseUrl({})).toBeNull(); + expect(databaseUrl({ [DATABASE_URL_ENV]: " " })).toBeNull(); + }); + + it("trims surrounding whitespace", () => { + expect( + databaseUrl({ [DATABASE_URL_ENV]: " postgres://db/termix " }), + ).toBe("postgres://db/termix"); + }); +}); + +describe("assertUrlMatchesDialect", () => { + it("accepts the schemes each engine answers to", () => { + expect(() => + assertUrlMatchesDialect("postgres://db/termix", "postgres"), + ).not.toThrow(); + expect(() => + assertUrlMatchesDialect("postgresql://db/termix", "postgres"), + ).not.toThrow(); + expect(() => + assertUrlMatchesDialect("mysql://db/termix", "mysql"), + ).not.toThrow(); + // MariaDB speaks the MySQL protocol. + expect(() => + assertUrlMatchesDialect("mariadb://db/termix", "mysql"), + ).not.toThrow(); + }); + + it("is case-insensitive about the scheme", () => { + expect(() => + assertUrlMatchesDialect("POSTGRES://db/termix", "postgres"), + ).not.toThrow(); + }); + + it("catches a mismatch and says what is wrong", () => { + // The failure mode this exists to prevent: a driver error thirty frames + // down that never mentions the actual misconfiguration. + expect(() => + assertUrlMatchesDialect("mysql://db/termix", "postgres"), + ).toThrow(/is a "mysql:\/\/" URL but DATABASE_DIALECT is "postgres"/); + + expect(() => + assertUrlMatchesDialect("postgres://db/termix", "mysql"), + ).toThrow(/expected one of mysql:\/\/, mariadb:\/\//i); + }); + + it("rejects sqlite, which does not use a URL", () => { + expect(() => + assertUrlMatchesDialect("postgres://db/termix", "sqlite"), + ).toThrow(/does not use DATABASE_URL/); + }); +}); + +describe("poolMax", () => { + it("defaults to the driver's own pool size", () => { + expect(poolMax({})).toBe(10); + expect(poolMax({ [DATABASE_POOL_MAX_ENV]: " " })).toBe(10); + }); + + it("takes a positive integer", () => { + expect(poolMax({ [DATABASE_POOL_MAX_ENV]: "25" })).toBe(25); + }); + + it("refuses a value that would silently produce a broken pool", () => { + for (const bad of ["0", "-1", "3.5", "lots"]) { + expect(() => poolMax({ [DATABASE_POOL_MAX_ENV]: bad })).toThrow( + /positive integer/, + ); + } + }); +}); + +describe("sslOption", () => { + it("is off unless asked for, so existing installs are unaffected", () => { + expect(sslOption({})).toBe(false); + expect(sslOption({ [DATABASE_SSL_ENV]: "false" })).toBe(false); + expect(sslOption({ [DATABASE_SSL_ENV]: "disable" })).toBe(false); + }); + + it("verifies the certificate for require", () => { + expect(sslOption({ [DATABASE_SSL_ENV]: "require" })).toEqual({ + rejectUnauthorized: true, + }); + expect(sslOption({ [DATABASE_SSL_ENV]: "TRUE" })).toEqual({ + rejectUnauthorized: true, + }); + }); + + it("allows a self-signed certificate only when told to skip verification", () => { + expect(sslOption({ [DATABASE_SSL_ENV]: "no-verify" })).toEqual({ + rejectUnauthorized: false, + }); + }); + + it("refuses a value it does not understand rather than quietly disabling TLS", () => { + expect(() => sslOption({ [DATABASE_SSL_ENV]: "maybe" })).toThrow( + /Unsupported DATABASE_SSL/, + ); + }); +}); + +describe("connectRemoteDatabase", () => { + it("validates pool settings before opening a connection", () => { + return expect( + connectRemoteDatabase("postgres", { + [DATABASE_URL_ENV]: "postgres://db/termix", + [DATABASE_POOL_MAX_ENV]: "nonsense", + }), + ).rejects.toThrow(/positive integer/); + }); + + it("refuses to connect without a URL, naming the variable", () => { + return expect(connectRemoteDatabase("postgres", {})).rejects.toThrow( + /DATABASE_URL must be set when DATABASE_DIALECT is "postgres"/, + ); + }); + + it("rejects a mismatched URL before opening a connection", () => { + return expect( + connectRemoteDatabase("postgres", { + [DATABASE_URL_ENV]: "mysql://db/termix", + }), + ).rejects.toThrow(/DATABASE_DIALECT is "postgres"/); + }); +}); diff --git a/src/backend/tests/database/db/migrate.test.ts b/src/backend/tests/database/db/migrate.test.ts new file mode 100644 index 0000000..d61eb7b --- /dev/null +++ b/src/backend/tests/database/db/migrate.test.ts @@ -0,0 +1,39 @@ +import path from "path"; +import { describe, expect, it } from "vitest"; +import { + migrationsFolder, + runRemoteMigrations, + MIGRATIONS_DIR_ENV, +} from "../../../database/db/migrate.js"; + +describe("migrationsFolder", () => { + it("gives each engine its own folder", () => { + // The generated SQL differs per dialect, so they cannot share one. + expect(migrationsFolder("postgres", {})).toBe( + path.resolve(process.cwd(), "drizzle", "postgres"), + ); + expect(migrationsFolder("mysql", {})).toBe( + path.resolve(process.cwd(), "drizzle", "mysql"), + ); + }); + + it("honours an explicit root", () => { + expect( + migrationsFolder("postgres", { [MIGRATIONS_DIR_ENV]: "/srv/migrations" }), + ).toBe(path.join("/srv/migrations", "postgres")); + }); + + it("ignores a blank override", () => { + expect(migrationsFolder("mysql", { [MIGRATIONS_DIR_ENV]: " " })).toBe( + path.resolve(process.cwd(), "drizzle", "mysql"), + ); + }); +}); + +describe("runRemoteMigrations", () => { + it("refuses sqlite, which builds its schema elsewhere", () => { + return expect( + runRemoteMigrations("sqlite", {} as never), + ).rejects.toThrow(/SQLite builds its schema in index.ts/); + }); +}); diff --git a/src/backend/tests/database/db/multi-dialect.test.ts b/src/backend/tests/database/db/multi-dialect.test.ts new file mode 100644 index 0000000..78b0061 --- /dev/null +++ b/src/backend/tests/database/db/multi-dialect.test.ts @@ -0,0 +1,258 @@ +import { describe, expect, it } from "vitest"; +import { eq } from "drizzle-orm"; +import { drizzle as sqliteDrizzle } from "drizzle-orm/better-sqlite3"; +import { drizzle as pgDrizzle } from "drizzle-orm/node-postgres"; +import { drizzle as mysqlDrizzle } from "drizzle-orm/mysql2"; +import { getTableConfig as sqliteTableConfig } from "drizzle-orm/sqlite-core"; +import { getTableConfig as pgTableConfig } from "drizzle-orm/pg-core"; +import { getTableConfig as mysqlTableConfig } from "drizzle-orm/mysql-core"; +import Database from "better-sqlite3"; +import * as sqliteSchema from "../../../database/db/schema.js"; +import * as pgSchema from "../../../database/db/schema.pg.js"; +import * as mysqlSchema from "../../../database/db/schema.mysql.js"; +import { PERFORMANCE_INDEXES } from "../../../database/db/performance-indexes.js"; +import { + DATABASE_DIALECT_ENV, + isDatabaseDialect, + needsExplicitPersist, + resolveDatabaseDialect, +} from "../../../database/db/dialect.js"; + +describe("resolveDatabaseDialect", () => { + it("defaults to sqlite so existing deployments are unaffected", () => { + expect(resolveDatabaseDialect({})).toBe("sqlite"); + expect(resolveDatabaseDialect({ [DATABASE_DIALECT_ENV]: "" })).toBe( + "sqlite", + ); + }); + + it("accepts the supported engines, case-insensitively", () => { + expect(resolveDatabaseDialect({ [DATABASE_DIALECT_ENV]: "postgres" })).toBe( + "postgres", + ); + expect(resolveDatabaseDialect({ [DATABASE_DIALECT_ENV]: "MySQL" })).toBe( + "mysql", + ); + }); + + it("refuses an unknown engine rather than silently using sqlite", () => { + expect(() => + resolveDatabaseDialect({ [DATABASE_DIALECT_ENV]: "oracle" }), + ).toThrow(/Unsupported/); + }); + + it("narrows correctly", () => { + expect(isDatabaseDialect("mysql")).toBe(true); + expect(isDatabaseDialect("mongo")).toBe(false); + }); +}); + +describe("needsExplicitPersist", () => { + it("is true only for sqlite", () => { + // SQLite runs in memory and is serialised back to an encrypted file, so + // every write needs a flush. The others have already committed durably. + expect(needsExplicitPersist("sqlite")).toBe(true); + expect(needsExplicitPersist("postgres")).toBe(false); + expect(needsExplicitPersist("mysql")).toBe(false); + }); +}); + +/** + * schema.pg.ts and schema.mysql.ts are generated from schema.ts. These check the + * generated output is usable rather than merely syntactically valid โ€” the + * repository layer's correctness rests on all three behaving the same way. + */ +describe("generated schemas", () => { + it("declares the same tables in all three dialects", () => { + const tablesOf = (schema: Record) => + Object.keys(schema).sort(); + + expect(tablesOf(pgSchema)).toEqual(tablesOf(sqliteSchema)); + expect(tablesOf(mysqlSchema)).toEqual(tablesOf(sqliteSchema)); + // Guard against a generator that silently emits nothing. + expect(tablesOf(sqliteSchema).length).toBeGreaterThan(40); + }); + + it("maps each column to the right storage type per dialect", () => { + expect(sqliteSchema.users.isAdmin.getSQLType()).toBe("integer"); + expect(pgSchema.users.isAdmin.getSQLType()).toBe("boolean"); + expect(mysqlSchema.users.isAdmin.getSQLType()).toBe("boolean"); + + // A primary key must be indexable, which rules out unbounded TEXT on MySQL. + expect(sqliteSchema.users.id.getSQLType()).toBe("text"); + expect(pgSchema.users.id.getSQLType()).toContain("varchar"); + expect(mysqlSchema.users.id.getSQLType()).toContain("varchar"); + }); + + it("spells the autoincrement key three different ways", () => { + expect(sqliteSchema.auditLogs.id.getSQLType()).toBe("integer"); + expect(pgSchema.auditLogs.id.getSQLType()).toBe("serial"); + expect(mysqlSchema.auditLogs.id.getSQLType()).toBe("int"); + + for (const schema of [sqliteSchema, pgSchema, mysqlSchema]) { + expect(schema.auditLogs.id.primary).toBe(true); + } + }); + + it("preserves both foreign-key behaviours", () => { + // 80 cascade + 12 set null across the schema; set null is what keeps the + // audit trail after a user is deleted (#1132). + const perDialect = [ + { schema: sqliteSchema, config: sqliteTableConfig }, + { schema: pgSchema, config: pgTableConfig }, + { schema: mysqlSchema, config: mysqlTableConfig }, + ] as const; + + for (const { schema, config } of perDialect) { + const read = config as (table: unknown) => { + foreignKeys: { onDelete?: string }[]; + }; + + const auditFks = read(schema.auditLogs).foreignKeys; + expect(auditFks).toHaveLength(1); + expect(auditFks[0].onDelete).toBe("set null"); + + const folderFks = read(schema.sshFolders).foreignKeys; + expect(folderFks.map((fk) => fk.onDelete).sort()).toEqual([ + "cascade", + "set null", + ]); + } + }); + + it("keeps nullability and uniqueness", () => { + for (const schema of [sqliteSchema, pgSchema, mysqlSchema]) { + expect(schema.auditLogs.userId.notNull).toBe(false); + expect(schema.sshFolders.userId.notNull).toBe(true); + expect(schema.sshFolders.syncId.isUnique).toBe(true); + } + }); + + /** + * SQLite builds its indexes at runtime from PERFORMANCE_INDEXES; Postgres and + * MySQL only ever get what the migrations declare, which comes from schema.ts. + * Anything listed in one and missing from the other is an index the engines + * chosen for scale silently do without โ€” which is how 31 of them went missing. + */ + it("declares every performance index in schema.ts", () => { + // Both are the leading column of an existing composite unique index, which + // already serves the same lookup. A second index would be redundant. + const coveredByCompositePrefix = new Set([ + "idx_user_roles_user_id", // idx_user_roles_user_role (user_id, role_id) + "idx_fleet_members_fleet", // idx_fleet_members_fleet_host (fleet_id, host_id) + ]); + + const declared = new Set( + Object.values(sqliteSchema) + .filter((table): table is object => typeof table === "object") + .flatMap((table) => { + try { + return sqliteTableConfig(table as never).indexes; + } catch { + return []; + } + }) + .map((index) => index.config.name), + ); + + const missing = PERFORMANCE_INDEXES.map((index) => index.name) + .filter((name) => !coveredByCompositePrefix.has(name)) + .filter((name) => !declared.has(name)); + + expect(missing).toEqual([]); + }); +}); + +/** + * Queries are built, never executed, so no server is required. What matters is + * that identical repository-style code produces correct SQL for each engine. + */ +describe("query generation per dialect", () => { + const sqliteDb = sqliteDrizzle(new Database(":memory:"), { + schema: sqliteSchema, + }); + const pgDb = pgDrizzle.mock({ schema: pgSchema }); + const mysqlDb = mysqlDrizzle.mock({ schema: mysqlSchema, mode: "default" }); + + it("quotes identifiers the way each engine expects", () => { + const built = [ + sqliteDb + .select() + .from(sqliteSchema.settings) + .where(eq(sqliteSchema.settings.key, "guac_url")) + .toSQL(), + pgDb + .select() + .from(pgSchema.settings) + .where(eq(pgSchema.settings.key, "guac_url")) + .toSQL(), + mysqlDb + .select() + .from(mysqlSchema.settings) + .where(eq(mysqlSchema.settings.key, "guac_url")) + .toSQL(), + ]; + + expect(built[0].sql).toContain('"settings"'); + expect(built[1].sql).toContain('"settings"'); + expect(built[2].sql).toContain("`settings`"); + + // The value is parameterised either way, never inlined. + for (const sql of built) { + expect(sql.params).toEqual(["guac_url"]); + } + }); + + it("uses each engine's placeholder style", () => { + expect( + pgDb + .select() + .from(pgSchema.users) + .where(eq(pgSchema.users.id, "u-1")) + .toSQL().sql, + ).toContain("$1"); + + expect( + mysqlDb + .select() + .from(mysqlSchema.users) + .where(eq(mysqlSchema.users.id, "u-1")) + .toSQL().sql, + ).toContain("?"); + }); + + it("stores booleans as the type each engine expects", () => { + const row = { id: "u-1", username: "alice", passwordHash: "hash" }; + + const sqliteSql = sqliteDb + .insert(sqliteSchema.users) + .values({ ...row, isAdmin: true }) + .toSQL(); + const pgSql = pgDb + .insert(pgSchema.users) + .values({ ...row, isAdmin: true }) + .toSQL(); + + // The storage difference the generator exists to absorb. + expect(sqliteSql.params).toContain(1); + expect(pgSql.params).toContain(true); + }); + + it("round-trips on the engine that is actually wired up", () => { + const sqlite = new Database(":memory:"); + sqlite.exec( + `CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);`, + ); + const db = sqliteDrizzle(sqlite, { schema: sqliteSchema }); + + db.insert(sqliteSchema.settings) + .values({ key: "guac_url", value: "guacd:4822" }) + .run(); + + expect(db.select().from(sqliteSchema.settings).all()).toEqual([ + { key: "guac_url", value: "guacd:4822" }, + ]); + + sqlite.close(); + }); +}); diff --git a/src/backend/tests/database/db/performance-indexes.test.ts b/src/backend/tests/database/db/performance-indexes.test.ts new file mode 100644 index 0000000..c24ddb5 --- /dev/null +++ b/src/backend/tests/database/db/performance-indexes.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it, vi } from "vitest"; +import Database from "better-sqlite3"; +import { + PERFORMANCE_INDEXES, + createPerformanceIndexes, +} from "../../../database/db/performance-indexes.js"; + +vi.mock("../../../utils/logger.js", () => ({ + databaseLogger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + success: vi.fn(), + }, +})); + +function seedSchema(db: Database.Database): void { + db.exec(` + CREATE TABLE ssh_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + folder TEXT, + parent_host_id INTEGER, + credential_id INTEGER + ); + CREATE TABLE host_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT, + role_id INTEGER, + expires_at TEXT + ); + CREATE TABLE audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + timestamp TEXT NOT NULL + ); + `); +} + +function indexNames(db: Database.Database): string[] { + return db + .prepare("SELECT name FROM sqlite_master WHERE type = 'index'") + .all() + .map((row) => (row as { name: string }).name); +} + +describe("createPerformanceIndexes", () => { + it("creates the indexes for tables that exist", () => { + const db = new Database(":memory:"); + seedSchema(db); + + const summary = createPerformanceIndexes(db, [ + { name: "idx_ssh_data_user_id", table: "ssh_data", columns: "user_id" }, + { + name: "idx_audit_logs_user_ts", + table: "audit_logs", + columns: "user_id, timestamp", + }, + ]); + + expect(summary).toMatchObject({ created: 2, skipped: 0, failed: 0 }); + expect(indexNames(db)).toEqual( + expect.arrayContaining(["idx_ssh_data_user_id", "idx_audit_logs_user_ts"]), + ); + + db.close(); + }); + + it("is safe to run repeatedly", () => { + const db = new Database(":memory:"); + seedSchema(db); + + const first = createPerformanceIndexes(db); + const second = createPerformanceIndexes(db); + + expect(second.created).toBe(first.created); + expect(second.failed).toBe(0); + + db.close(); + }); + + it("skips tables this install has not created instead of failing", () => { + const db = new Database(":memory:"); + seedSchema(db); + + const summary = createPerformanceIndexes(db, [ + { name: "idx_missing", table: "not_a_table", columns: "user_id" }, + { name: "idx_ssh_data_user_id", table: "ssh_data", columns: "user_id" }, + ]); + + expect(summary).toMatchObject({ created: 1, skipped: 1, failed: 0 }); + + db.close(); + }); + + it("skips columns an older schema has not added yet", () => { + const db = new Database(":memory:"); + db.exec("CREATE TABLE ssh_data (id INTEGER PRIMARY KEY, user_id TEXT)"); + + const summary = createPerformanceIndexes(db, [ + { + name: "idx_ssh_data_parent_host", + table: "ssh_data", + columns: "parent_host_id", + }, + ]); + + expect(summary).toMatchObject({ created: 0, skipped: 1, failed: 0 }); + + db.close(); + }); + + it("actually uses the index for the host list query", () => { + const db = new Database(":memory:"); + seedSchema(db); + createPerformanceIndexes(db); + + const plan = db + .prepare("EXPLAIN QUERY PLAN SELECT * FROM ssh_data WHERE user_id = ?") + .all("user-1") + .map((row) => (row as { detail: string }).detail) + .join(" "); + + expect(plan).toContain("idx_ssh_data_user_id"); + + db.close(); + }); + + it("uses a single index to satisfy the audit log filter and its ordering", () => { + const db = new Database(":memory:"); + seedSchema(db); + createPerformanceIndexes(db); + + const plan = db + .prepare( + "EXPLAIN QUERY PLAN SELECT * FROM audit_logs WHERE user_id = ? ORDER BY timestamp DESC LIMIT 50", + ) + .all("user-1") + .map((row) => (row as { detail: string }).detail) + .join(" "); + + expect(plan).toContain("idx_audit_logs_user_ts"); + // Leading with the filtered column means the index also provides the order. + expect(plan).not.toContain("TEMP B-TREE"); + + db.close(); + }); + + it("declares unique index names", () => { + const names = PERFORMANCE_INDEXES.map((index) => index.name); + expect(new Set(names).size).toBe(names.length); + }); +}); diff --git a/src/backend/tests/database/db/proxmox-stats-columns-migration.test.ts b/src/backend/tests/database/db/proxmox-stats-columns-migration.test.ts new file mode 100644 index 0000000..efdd1fc --- /dev/null +++ b/src/backend/tests/database/db/proxmox-stats-columns-migration.test.ts @@ -0,0 +1,110 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * The Proxmox Stats feature adds `enable_proxmox_stats` and + * `proxmox_stats_config` to `ssh_data`, backfilled via `addColumnIfNotExists` + * next to the existing `enable_proxmox`/`proxmox_config` columns. Verify the + * migration adds both columns, with the right default, on a database that + * predates them. + */ +describe("proxmox stats columns migration", () => { + let dataDir: string; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-proxmox-stats-")); + vi.resetModules(); + process.env.DATA_DIR = dataDir; + process.env.DB_FILE_ENCRYPTION = "false"; + process.env.ALLOW_EMPTY_DATA_DIR = "true"; + }); + + afterEach(() => { + delete process.env.DATA_DIR; + delete process.env.DB_FILE_ENCRYPTION; + delete process.env.ALLOW_EMPTY_DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + function writePreUpgradeDatabase(): void { + const seed = new Database(":memory:"); + seed.exec(` + CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password_hash TEXT NOT NULL + ); + + CREATE TABLE ssh_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT, + ip TEXT NOT NULL, + port INTEGER NOT NULL, + username TEXT NOT NULL, + auth_type TEXT NOT NULL DEFAULT 'password', + enable_proxmox INTEGER NOT NULL DEFAULT 0, + proxmox_config TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + INSERT INTO users (id, username, password_hash) + VALUES ('owner', 'alice', 'hash'); + + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type, enable_proxmox) + VALUES (1, 'owner', 'pve node', '10.0.0.9', 22, 'root', 'password', 1); + `); + fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize()); + seed.close(); + } + + it("adds enable_proxmox_stats (default 0) and proxmox_stats_config (nullable) columns", async () => { + writePreUpgradeDatabase(); + + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + const sqlite = db.getSqlite(); + + const columns = sqlite + .prepare("PRAGMA table_info(ssh_data)") + .all() as Array<{ name: string; notnull: number; dflt_value: string | null }>; + + const enableCol = columns.find((c) => c.name === "enable_proxmox_stats"); + expect(enableCol).toBeDefined(); + expect(enableCol?.notnull).toBe(1); + + const configCol = columns.find((c) => c.name === "proxmox_stats_config"); + expect(configCol).toBeDefined(); + expect(configCol?.notnull).toBe(0); + + const row = sqlite + .prepare( + "SELECT enable_proxmox_stats, proxmox_stats_config FROM ssh_data WHERE id = 1", + ) + .get() as { enable_proxmox_stats: number; proxmox_stats_config: string | null }; + + // Pre-existing rows default to disabled, independent of enable_proxmox. + expect(row.enable_proxmox_stats).toBe(0); + expect(row.proxmox_stats_config).toBeNull(); + }); + + it("creates the proxmox_node_history and proxmox_stats_preferences tables", async () => { + writePreUpgradeDatabase(); + + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + const sqlite = db.getSqlite(); + + const tables = sqlite + .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") + .pluck() + .all() as string[]; + + expect(tables).toContain("proxmox_node_history"); + expect(tables).toContain("proxmox_stats_preferences"); + }); +}); diff --git a/src/backend/tests/database/db/share-ssh-auth-backfill.test.ts b/src/backend/tests/database/db/share-ssh-auth-backfill.test.ts new file mode 100644 index 0000000..bf79203 --- /dev/null +++ b/src/backend/tests/database/db/share-ssh-auth-backfill.test.ts @@ -0,0 +1,128 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * Sharing a host used to hand the owner's SSH authentication to the recipient + * unconditionally. 2.6.1 put that behind `ssh_data.share_ssh_auth`, added as + * `NOT NULL DEFAULT 0` โ€” so every host shared before the upgrade silently + * stopped supplying credentials, and recipients hit "No valid authentication + * method provided" on hosts that had worked the day before. + * + * Hosts that are already shared get the flag turned on, because that is where + * the old behaviour was in effect. Hosts nobody has shared keep the new + * default: their owner decides when they share one. + */ +describe("share_ssh_auth backfill", () => { + let dataDir: string; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-share-auth-")); + vi.resetModules(); + process.env.DATA_DIR = dataDir; + process.env.DB_FILE_ENCRYPTION = "false"; + process.env.ALLOW_EMPTY_DATA_DIR = "true"; + }); + + afterEach(() => { + delete process.env.DATA_DIR; + delete process.env.DB_FILE_ENCRYPTION; + delete process.env.ALLOW_EMPTY_DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + /** A 2.6.0 database: hosts and shares exist, the column does not. */ + function writePreUpgradeDatabase(): void { + const seed = new Database(":memory:"); + seed.exec(` + CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password_hash TEXT NOT NULL + ); + + CREATE TABLE ssh_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT, + ip TEXT NOT NULL, + port INTEGER NOT NULL, + username TEXT NOT NULL, + folder TEXT, + tags TEXT, + pin INTEGER NOT NULL DEFAULT 0, + auth_type TEXT NOT NULL DEFAULT 'password', + enable_terminal INTEGER NOT NULL DEFAULT 1, + enable_tunnel INTEGER NOT NULL DEFAULT 0, + enable_file_manager INTEGER NOT NULL DEFAULT 1, + default_path TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE host_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT, + role_id INTEGER, + granted_by TEXT NOT NULL, + permission_level TEXT NOT NULL DEFAULT 'use', + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + INSERT INTO users (id, username, password_hash) + VALUES ('owner', 'alice', 'hash'), ('recipient', 'bob', 'hash'); + + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'owner', 'shared box', '10.0.0.7', 22, 'root', 'credential'), + (2, 'owner', 'private box', '10.0.0.8', 22, 'root', 'credential'); + + INSERT INTO host_access (host_id, user_id, granted_by, permission_level) + VALUES (1, 'recipient', 'owner', 'use'); + `); + fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize()); + seed.close(); + } + + function shareFlags( + sqlite: Database.Database, + ): Record { + const rows = sqlite + .prepare("SELECT id, share_ssh_auth FROM ssh_data ORDER BY id") + .all() as Array<{ id: number; share_ssh_auth: number }>; + return Object.fromEntries(rows.map((r) => [r.id, r.share_ssh_auth])); + } + + it("keeps already-shared hosts sharing their authentication", async () => { + writePreUpgradeDatabase(); + + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + + const flags = shareFlags(db.getSqlite()); + expect(flags[1]).toBe(1); // shared before the upgrade + expect(flags[2]).toBe(0); // never shared, new default stands + }); + + it("does not undo an owner who later turns it back off", async () => { + writePreUpgradeDatabase(); + + const first = await import("../../../database/db/index.js"); + await first.initializeDatabase(); + first + .getSqlite() + .prepare("UPDATE ssh_data SET share_ssh_auth = 0 WHERE id = 1") + .run(); + await first.saveMemoryDatabaseToFile?.(); + + // Second startup: the backfill is recorded as done and must not re-run. + vi.resetModules(); + const second = await import("../../../database/db/index.js"); + await second.initializeDatabase(); + + expect(shareFlags(second.getSqlite())[1]).toBe(0); + }); +}); diff --git a/src/backend/tests/database/db/ssh-credentials-username-rebuild.test.ts b/src/backend/tests/database/db/ssh-credentials-username-rebuild.test.ts new file mode 100644 index 0000000..fe5a8af --- /dev/null +++ b/src/backend/tests/database/db/ssh-credentials-username-rebuild.test.ts @@ -0,0 +1,167 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * `ssh_credentials.username` became nullable when key-only credentials landed, + * and databases created before that are rebuilt on startup to drop the + * constraint โ€” SQLite cannot ALTER a column. + * + * The rebuild restated the table's columns as a literal, then copied rows with + * `INSERT INTO temp SELECT `. The table has gained columns + * since (cert_public_key, pin, sort_order, sync_id), so the literal was + * narrower than the source: the INSERT failed on a column count mismatch, the + * error was swallowed as a warning, and the constraint survived every restart. + * + * Deriving the replacement table from `sqlite_master` keeps the two in step by + * construction. DROP TABLE also discards the table's indexes, so those are + * replayed rather than left to whatever runs later. + */ +describe("ssh_credentials username rebuild", () => { + let dataDir: string; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-cred-rebuild-")); + vi.resetModules(); + process.env.DATA_DIR = dataDir; + process.env.DB_FILE_ENCRYPTION = "false"; + process.env.ALLOW_EMPTY_DATA_DIR = "true"; + }); + + afterEach(() => { + delete process.env.DATA_DIR; + delete process.env.DB_FILE_ENCRYPTION; + delete process.env.ALLOW_EMPTY_DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + /** + * A database as a 2.6.x run leaves it: the old NOT NULL constraint is still + * there, but the columns added since are present, as is the sync_id index. + */ + function writeDatabaseNeedingRebuild(): void { + const seed = new Database(":memory:"); + seed.exec(` + CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password_hash TEXT NOT NULL + ); + + INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'); + + CREATE TABLE ssh_credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + folder TEXT, + tags TEXT, + auth_type TEXT NOT NULL, + username TEXT NOT NULL, + password TEXT, + key TEXT, + key_password TEXT, + key_type TEXT, + usage_count INTEGER NOT NULL DEFAULT 0, + last_used TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + private_key TEXT, + public_key TEXT, + detected_key_type TEXT, + cert_public_key TEXT, + pin INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER, + sync_id TEXT, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE UNIQUE INDEX idx_ssh_credentials_sync_id ON ssh_credentials(sync_id); + `); + seed + .prepare( + `INSERT INTO ssh_credentials (user_id, name, auth_type, username, pin, sort_order, sync_id) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .run("user-1", "prod box", "password", "root", 1, 3, "sync-abc"); + fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize()); + seed.close(); + } + + async function bootAndGetSqlite(): Promise { + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + return db.getSqlite(); + } + + function usernameIsNotNull(sqlite: Database.Database): boolean { + const columns = sqlite.prepare("PRAGMA table_info(ssh_credentials)").all() as Array<{ + name: string; + notnull: number; + }>; + return columns.find((col) => col.name === "username")?.notnull === 1; + } + + it("drops the constraint even though the table outgrew the old column list", async () => { + writeDatabaseNeedingRebuild(); + + const sqlite = await bootAndGetSqlite(); + + expect(usernameIsNotNull(sqlite)).toBe(false); + + expect(() => + sqlite + .prepare( + `INSERT INTO ssh_credentials (user_id, name, auth_type, key) + VALUES (?, ?, ?, ?)`, + ) + .run("user-1", "key only", "key", "PRIVATE KEY"), + ).not.toThrow(); + }); + + it("carries every column across, including the ones added after the rebuild was written", async () => { + writeDatabaseNeedingRebuild(); + + const sqlite = await bootAndGetSqlite(); + + const row = sqlite.prepare("SELECT * FROM ssh_credentials WHERE name = ?").get("prod box") as { + user_id: string; + username: string; + auth_type: string; + pin: number; + sort_order: number; + sync_id: string; + }; + + expect(row.user_id).toBe("user-1"); + expect(row.username).toBe("root"); + expect(row.auth_type).toBe("password"); + expect(row.pin).toBe(1); + expect(row.sort_order).toBe(3); + // sync_id identifies the row to remote sync; losing it re-keys the record. + expect(row.sync_id).toBe("sync-abc"); + }); + + it("keeps the sync_id uniqueness that DROP TABLE would otherwise discard", async () => { + writeDatabaseNeedingRebuild(); + + const sqlite = await bootAndGetSqlite(); + + const indexes = sqlite + .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'ssh_credentials'") + .pluck() + .all() as string[]; + expect(indexes).toContain("idx_ssh_credentials_sync_id"); + + sqlite + .prepare("INSERT INTO ssh_credentials (user_id, name, auth_type, sync_id) VALUES (?, ?, ?, ?)") + .run("user-1", "other box", "key", "sync-xyz"); + + expect(() => + sqlite.prepare("UPDATE ssh_credentials SET sync_id = ? WHERE name = ?").run("sync-abc", "other box"), + ).toThrow(/UNIQUE/i); + }); +}); diff --git a/src/backend/tests/database/db/unencrypted-persistence.test.ts b/src/backend/tests/database/db/unencrypted-persistence.test.ts new file mode 100644 index 0000000..1e0a011 --- /dev/null +++ b/src/backend/tests/database/db/unencrypted-persistence.test.ts @@ -0,0 +1,80 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * `DB_FILE_ENCRYPTION=false` used to mean "start empty, every time". + * + * The database lives in memory on every backend and is serialised to disk after + * writes; the flag only decides whether that file is ciphertext. The plain + * branch wrote `db.sqlite` faithfully and then never read it back, so each + * restart began with an empty database and silently discarded everything the + * previous run had saved. The data-dir guard made it worse by confirming a + * database was present in DATA_DIR immediately before it was thrown away. + */ +describe("unencrypted database persistence", () => { + let dataDir: string; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-plain-db-")); + vi.resetModules(); + process.env.DATA_DIR = dataDir; + process.env.DB_FILE_ENCRYPTION = "false"; + process.env.ALLOW_EMPTY_DATA_DIR = "true"; + }); + + afterEach(() => { + delete process.env.DATA_DIR; + delete process.env.DB_FILE_ENCRYPTION; + delete process.env.ALLOW_EMPTY_DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + /** A database file with one row, as a previous run would have left it. */ + function writeExistingDatabase(): void { + const seed = new Database(":memory:"); + seed.exec("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)"); + seed + .prepare("INSERT INTO settings (key, value) VALUES (?, ?)") + .run("survives_restart", "yes"); + fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize()); + seed.close(); + } + + it("reads back what an earlier run wrote", async () => { + writeExistingDatabase(); + + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + + const row = db + .getSqlite() + .prepare("SELECT value FROM settings WHERE key = ?") + .get("survives_restart") as { value: string } | undefined; + + expect(row?.value).toBe("yes"); + }); + + it("starts empty when there is no file yet", async () => { + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + + // Startup creates its own tables; the point is that it does not throw on a + // missing file and does not carry rows over from nowhere. + const row = db + .getSqlite() + .prepare("SELECT COUNT(*) AS count FROM users") + .get() as { count: number }; + + expect(row.count).toBe(0); + }); + + it("ignores a zero-length file rather than failing to open it", async () => { + fs.writeFileSync(path.join(dataDir, "db.sqlite"), ""); + + const db = await import("../../../database/db/index.js"); + await expect(db.initializeDatabase()).resolves.not.toThrow(); + }); +}); diff --git a/src/backend/tests/database/repositories/alert-repository.test.ts b/src/backend/tests/database/repositories/alert-repository.test.ts index e5fefb8..8987913 100644 --- a/src/backend/tests/database/repositories/alert-repository.test.ts +++ b/src/backend/tests/database/repositories/alert-repository.test.ts @@ -1,6 +1,9 @@ -import { afterEach, describe, expect, it } from "vitest"; +import crypto from "node:crypto"; +import { sql } from "drizzle-orm"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { TestSqliteDatabase } from "./test-support.js"; import { AlertRepository } from "../../../database/repositories/alert-repository.js"; +import { DataCrypto } from "../../../utils/data-crypto.js"; describe("AlertRepository", () => { let adapter: TestSqliteDatabase | null = null; @@ -17,68 +20,11 @@ describe("AlertRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT, - ip TEXT NOT NULL - ); - - CREATE TABLE alert_rules ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER, - name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1, - trigger_type TEXT NOT NULL, - threshold_value REAL, - threshold_duration_seconds INTEGER, - cooldown_minutes INTEGER NOT NULL DEFAULT 15, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE notification_channels ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL, - config TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE alert_rule_channels ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - rule_id INTEGER NOT NULL, - channel_id INTEGER NOT NULL - ); - - CREATE TABLE alert_firings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - rule_id INTEGER NOT NULL, - host_id INTEGER NOT NULL, - host_name TEXT NOT NULL, - fired_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - resolved_at TEXT, - value REAL, - message TEXT NOT NULL, - severity TEXT NOT NULL DEFAULT 'warning', - acknowledged INTEGER NOT NULL DEFAULT 0 - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO ssh_data (id, user_id, name, ip) - VALUES (1, 'user-1', 'alpha', '127.0.0.1'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'alpha', '127.0.0.1', 22, 'root', 'password'); `); return new AlertRepository(context, onWrite); @@ -229,7 +175,7 @@ describe("AlertRepository", () => { expect(unacknowledged.total).toBe(0); await repo.acknowledgeAllFirings("user-1"); - repo.pruneFiringsOlderThan("user-1", 0); + await repo.pruneFiringsOlderThan("user-1", 0); }); it("loads enabled rules and notification channels for the alert engine", async () => { @@ -283,6 +229,152 @@ describe("AlertRepository", () => { ]); }); + it("keeps another user's wildcard rules off a host they do not own", async () => { + const repo = await createRepository(); + await adapter!.exec(` + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (2, 'user-2', 'bravo', '127.0.0.2', 22, 'root', 'password'); + `); + + const ownerRule = await repo.createAlertRule({ + userId: "user-1", + hostId: null, + name: "Owner wildcard", + enabled: true, + triggerType: "cpu_threshold", + thresholdValue: 90, + thresholdDurationSeconds: 0, + cooldownMinutes: 15, + channels: [], + now: "2026-01-01T00:00:00.000Z", + }); + const otherRule = await repo.createAlertRule({ + userId: "user-2", + hostId: null, + name: "Other wildcard", + enabled: true, + triggerType: "cpu_threshold", + thresholdValue: 90, + thresholdDurationSeconds: 0, + cooldownMinutes: 15, + channels: [], + now: "2026-01-01T00:00:00.000Z", + }); + + // Host 1 belongs to user-1, so only user-1's wildcard rule may fire. + const forHostOne = await repo.listEnabledRulesForHost(1); + expect(forHostOne.map((rule) => rule.id)).toEqual([ownerRule.id]); + + const forHostTwo = await repo.listEnabledRulesForHost(2); + expect(forHostTwo.map((rule) => rule.id)).toEqual([otherRule.id]); + }); + + it("still matches a rule pinned to a specific host", async () => { + const repo = await createRepository(); + const pinned = await repo.createAlertRule({ + userId: "user-1", + hostId: 1, + name: "Pinned", + enabled: true, + triggerType: "disk_threshold", + thresholdValue: 90, + thresholdDurationSeconds: 0, + cooldownMinutes: 15, + channels: [], + now: "2026-01-01T00:00:00.000Z", + }); + + expect((await repo.listEnabledRulesForHost(1)).map((r) => r.id)).toEqual([ + pinned.id, + ]); + }); + + it("encrypts channel configs at rest and returns them decrypted", async () => { + const key = crypto.randomBytes(32); + const spy = vi + .spyOn(DataCrypto, "getUserDataKey") + .mockImplementation(() => key); + + try { + const repo = await createRepository(); + const secret = '{"url":"https://ntfy.test","token":"super-secret"}'; + + const created = await repo.createNotificationChannel({ + userId: "user-1", + name: "Ntfy", + type: "ntfy", + config: secret, + enabled: true, + }); + expect(created.config).toBe(secret); + + // The stored bytes must not contain the token in the clear. + const stored = await adapter!.query<{ config: string }>( + sql`SELECT config FROM notification_channels WHERE id = ${created.id}`, + ); + expect(stored[0].config).not.toContain("super-secret"); + + // Every read path hands back plaintext. + const listed = await repo.listNotificationChannels("user-1"); + expect(listed[0].config).toBe(secret); + expect( + (await repo.findNotificationChannelForUser(created.id, "user-1")) + ?.config, + ).toBe(secret); + + const rule = await repo.createAlertRule({ + userId: "user-1", + hostId: null, + name: "CPU", + enabled: true, + triggerType: "cpu_threshold", + thresholdValue: 90, + thresholdDurationSeconds: 0, + cooldownMinutes: 15, + channels: [created.id], + now: "2026-01-01T00:00:00.000Z", + }); + const engineChannels = await repo.listEnabledChannelsForRule(rule.id); + expect(engineChannels[0].config).toBe(secret); + + const rotated = '{"url":"https://ntfy.test","token":"rotated"}'; + const updated = await repo.updateNotificationChannel( + created.id, + "user-1", + { config: rotated }, + ); + expect(updated?.config).toBe(rotated); + } finally { + spy.mockRestore(); + } + }); + + it("still reads channel configs written before encryption", async () => { + const repo = await createRepository(); + const plaintext = '{"url":"https://legacy.test"}'; + const created = await repo.createNotificationChannel({ + userId: "user-1", + name: "Legacy", + type: "webhook", + config: plaintext, + enabled: true, + }); + + const key = crypto.randomBytes(32); + const spy = vi + .spyOn(DataCrypto, "getUserDataKey") + .mockImplementation(() => key); + try { + const found = await repo.findNotificationChannelForUser( + created.id, + "user-1", + ); + expect(found?.config).toBe(plaintext); + } finally { + spy.mockRestore(); + } + }); + it("loads host display names for alert payloads", async () => { const repo = await createRepository(); diff --git a/src/backend/tests/database/repositories/api-key-repository.test.ts b/src/backend/tests/database/repositories/api-key-repository.test.ts index 888460f..c43d4bd 100644 --- a/src/backend/tests/database/repositories/api-key-repository.test.ts +++ b/src/backend/tests/database/repositories/api-key-repository.test.ts @@ -17,28 +17,7 @@ describe("ApiKeyRepository", () => { }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE api_keys ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - token_hash TEXT NOT NULL, - token_prefix TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT, - last_used_at TEXT, - is_active INTEGER NOT NULL DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'admin', 'hash'), ('user-2', 'target', 'hash'); diff --git a/src/backend/tests/database/repositories/audit-log-repository.test.ts b/src/backend/tests/database/repositories/audit-log-repository.test.ts index 2f1fed7..3980bda 100644 --- a/src/backend/tests/database/repositories/audit-log-repository.test.ts +++ b/src/backend/tests/database/repositories/audit-log-repository.test.ts @@ -1,11 +1,19 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { sql } from "drizzle-orm"; import { TestSqliteDatabase } from "./test-support.js"; import { AuditLogRepository } from "../../../database/repositories/audit-log-repository.js"; describe("AuditLogRepository", () => { let adapter: TestSqliteDatabase | null = null; + beforeEach(() => { + AuditLogRepository.resetPruneThrottleForTests(); + }); + afterEach(async () => { + delete process.env.AUDIT_LOG_MAX_ENTRIES; + delete process.env.AUDIT_LOG_RETENTION_DAYS; + AuditLogRepository.resetPruneThrottleForTests(); if (adapter) { await adapter.close(); adapter = null; @@ -17,31 +25,7 @@ describe("AuditLogRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE audit_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - username TEXT NOT NULL, - action TEXT NOT NULL, - resource_type TEXT NOT NULL, - resource_id TEXT, - resource_name TEXT, - details TEXT, - ip_address TEXT, - user_agent TEXT, - success INTEGER NOT NULL, - error_message TEXT, - timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); `); @@ -129,4 +113,164 @@ describe("AuditLogRepository", () => { ).logs.map((log) => log.userId), ).toEqual(["user-2"]); }); + + it("keeps entries when their user is deleted, detaching instead of removing", async () => { + const repo = await createRepository(); + + await repo.create({ + userId: "user-1", + username: "alice", + action: "delete_host", + resourceType: "host", + resourceId: "9", + success: true, + timestamp: "2026-07-01T00:00:00.000Z", + }); + await repo.create({ + userId: "user-2", + username: "bob", + action: "create_host", + resourceType: "host", + resourceId: "8", + success: true, + timestamp: "2026-07-02T00:00:00.000Z", + }); + + expect(await repo.anonymizeByUserId("user-1")).toBe(1); + + const { logs } = await repo.listPage({ filters: {}, limit: 10, offset: 0 }); + expect(logs).toHaveLength(2); + + const detached = logs.find((log) => log.action === "delete_host"); + // The account is gone; the entry and its actor name are not. + expect(detached?.userId).toBeNull(); + expect(detached?.username).toBe("alice"); + expect(logs.find((log) => log.action === "create_host")?.userId).toBe( + "user-2", + ); + }); + + it("reports nothing to detach for a user with no entries", async () => { + const repo = await createRepository(); + + expect(await repo.anonymizeByUserId("user-2")).toBe(0); + }); + + describe("pruning", () => { + async function countEntries(): Promise { + const rows = await adapter!.query<{ count: number }>( + sql`SELECT COUNT(*) AS count FROM audit_logs`, + ); + return Number(rows[0].count); + } + + let writeSeq = 0; + beforeEach(() => { + writeSeq = 0; + }); + + async function write(repo: AuditLogRepository, n: number): Promise { + for (let i = 0; i < n; i++) { + await repo.create({ + userId: "user-1", + username: "alice", + action: "host_connect", + resourceType: "host", + success: true, + // Monotonic across calls so a second batch never reuses timestamps + // from the first, which would make "oldest first" ambiguous. + timestamp: new Date( + Date.UTC(2026, 0, 1, 0, 0, writeSeq++), + ).toISOString(), + }); + } + } + + it("enforces the entry cap down to the target ratio", async () => { + process.env.AUDIT_LOG_MAX_ENTRIES = "10"; + const repo = await createRepository(); + + await write(repo, 10); + await repo.pruneNow(); + + // Cap 10, target ratio 0.9 -> trimmed back to 9. + expect(await countEntries()).toBe(9); + }); + + it("drops the oldest entries first when over the cap", async () => { + process.env.AUDIT_LOG_MAX_ENTRIES = "10"; + const repo = await createRepository(); + + await write(repo, 10); + await repo.pruneNow(); + + const rows = await adapter!.query<{ timestamp: string }>( + sql`SELECT timestamp FROM audit_logs ORDER BY timestamp ASC`, + ); + // The first-written entry is the one discarded. + expect(rows[0].timestamp).toBe( + new Date(Date.UTC(2026, 0, 1, 0, 0, 1)).toISOString(), + ); + }); + + it("removes entries past the retention window", async () => { + process.env.AUDIT_LOG_RETENTION_DAYS = "1"; + const repo = await createRepository(); + + await repo.create({ + username: "alice", + action: "old", + resourceType: "host", + success: true, + timestamp: new Date(Date.now() - 5 * 86_400_000).toISOString(), + }); + await repo.create({ + username: "alice", + action: "fresh", + resourceType: "host", + success: true, + timestamp: new Date().toISOString(), + }); + + await repo.pruneNow(); + + const rows = await adapter!.query<{ action: string }>( + sql`SELECT action FROM audit_logs`, + ); + expect(rows.map((row) => row.action)).toEqual(["fresh"]); + }); + + it("keeps enforcing the cap across a burst of writes", async () => { + process.env.AUDIT_LOG_MAX_ENTRIES = "10"; + const repo = await createRepository(); + + // The cap is checked on the write that crosses it, not on a timer, so a + // burst cannot run the table away past the ceiling. + await write(repo, 24); + + expect(await countEntries()).toBeLessThanOrEqual(10); + }); + + it("re-reads the row count after entries are deleted elsewhere", async () => { + process.env.AUDIT_LOG_MAX_ENTRIES = "10"; + const repo = await createRepository(); + + await write(repo, 9); + // Clears the table, so the cached count is now far too high. + await repo.deleteByUserId("user-1"); + await write(repo, 9); + + // Had the count kept counting up from 9, this second batch would have + // tripped the cap and pruned; a correct re-read leaves all 9 in place. + expect(await countEntries()).toBe(9); + }); + + it("keeps the write succeeding even when pruning cannot run", async () => { + process.env.AUDIT_LOG_MAX_ENTRIES = "not-a-number"; + const repo = await createRepository(); + + await expect(write(repo, 1)).resolves.toBeUndefined(); + expect(await countEntries()).toBe(1); + }); + }); }); diff --git a/src/backend/tests/database/repositories/audit-log-retention.test.ts b/src/backend/tests/database/repositories/audit-log-retention.test.ts new file mode 100644 index 0000000..e0a290c --- /dev/null +++ b/src/backend/tests/database/repositories/audit-log-retention.test.ts @@ -0,0 +1,161 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const logs = vi.hoisted(() => ({ info: vi.fn(), warn: vi.fn() })); + +vi.mock("../../../utils/logger.js", () => ({ + databaseLogger: logs, +})); + +const { TestSqliteDatabase } = await import("./test-support.js"); +const { + AuditLogRepository, + auditRetentionDays, + auditMaxEntries, + AUDIT_RETENTION_DAYS_ENV, + AUDIT_MAX_ENTRIES_ENV, +} = await import("../../../database/repositories/audit-log-repository.js"); + +let adapter: InstanceType | null = null; +const savedEnv: Record = {}; + +beforeEach(() => { + logs.info.mockReset(); + logs.warn.mockReset(); + for (const key of [AUDIT_RETENTION_DAYS_ENV, AUDIT_MAX_ENTRIES_ENV]) { + savedEnv[key] = process.env[key]; + delete process.env[key]; + } +}); + +afterEach(async () => { + for (const [key, value] of Object.entries(savedEnv)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + if (adapter) { + await adapter.close(); + adapter = null; + } +}); + +async function createRepository() { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) VALUES + ('u-1', 'u-1', 'hash'); + `); + return new AuditLogRepository(context); +} + +function daysAgo(days: number): string { + const d = new Date(Date.now() - days * 24 * 60 * 60 * 1000); + return d.toISOString().slice(0, 19).replace("T", " "); +} + +async function seed( + repo: Awaited>, + timestamp: string, + action = "create_host", +) { + await repo.create({ + userId: "u-1", + username: "alice", + action, + resourceType: "host", + success: true, + timestamp, + }); +} + +describe("audit retention configuration", () => { + it("has no time limit unless one is configured", () => { + expect(auditRetentionDays({})).toBeNull(); + expect(auditRetentionDays({ [AUDIT_RETENTION_DAYS_ENV]: "90" })).toBe(90); + }); + + it("ignores values that are not a positive count", () => { + for (const bad of ["0", "-5", "", "abc"]) { + expect( + auditRetentionDays({ [AUDIT_RETENTION_DAYS_ENV]: bad }), + ).toBeNull(); + } + }); + + it("falls back to the built-in cap", () => { + expect(auditMaxEntries({})).toBe(10000); + expect(auditMaxEntries({ [AUDIT_MAX_ENTRIES_ENV]: "250" })).toBe(250); + expect(auditMaxEntries({ [AUDIT_MAX_ENTRIES_ENV]: "-1" })).toBe(10000); + }); +}); + +describe("audit retention pruning", () => { + it("keeps everything when no retention is set", async () => { + const repo = await createRepository(); + + await seed(repo, daysAgo(400)); + await seed(repo, daysAgo(1)); + + const { total } = await repo.listPage({ + filters: {}, + limit: 10, + offset: 0, + }); + expect(total).toBe(2); + expect(logs.info).not.toHaveBeenCalled(); + }); + + it("drops entries past the retention window and says so", async () => { + process.env[AUDIT_RETENTION_DAYS_ENV] = "30"; + const repo = await createRepository(); + + await seed(repo, daysAgo(90), "old_action"); + await seed(repo, daysAgo(5), "recent_action"); + + const { logs: rows } = await repo.listPage({ + filters: {}, + limit: 10, + offset: 0, + }); + expect(rows.map((r) => r.action)).toEqual(["recent_action"]); + + expect(logs.info).toHaveBeenCalledWith( + expect.stringContaining("past retention"), + expect.objectContaining({ operation: "audit_retention_prune" }), + ); + }); + + it("warns when the row cap discards entries still inside the window", async () => { + process.env[AUDIT_MAX_ENTRIES_ENV] = "5"; + const repo = await createRepository(); + + for (let i = 0; i < 6; i++) { + await seed(repo, daysAgo(10 - i), `action_${i}`); + } + + // The cap is not a retention policy: these entries were still current. + expect(logs.warn).toHaveBeenCalledWith( + expect.stringContaining("cap"), + expect.objectContaining({ + operation: "audit_overflow_prune", + maxEntries: 5, + }), + ); + + const { total } = await repo.listPage({ + filters: {}, + limit: 20, + offset: 0, + }); + expect(total).toBeLessThan(6); + }); + + it("stays quiet while under the cap", async () => { + process.env[AUDIT_MAX_ENTRIES_ENV] = "100"; + const repo = await createRepository(); + + await seed(repo, daysAgo(1)); + + expect(logs.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/database/repositories/c2s-tunnel-preset-repository.test.ts b/src/backend/tests/database/repositories/c2s-tunnel-preset-repository.test.ts index 0e3196d..5922ca4 100644 --- a/src/backend/tests/database/repositories/c2s-tunnel-preset-repository.test.ts +++ b/src/backend/tests/database/repositories/c2s-tunnel-preset-repository.test.ts @@ -17,24 +17,7 @@ describe("C2sTunnelPresetRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE c2s_tunnel_presets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - config TEXT NOT NULL, - platform TEXT, - computer_name TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); `); diff --git a/src/backend/tests/database/repositories/command-history-repository.test.ts b/src/backend/tests/database/repositories/command-history-repository.test.ts index ea58d02..83e821a 100644 --- a/src/backend/tests/database/repositories/command-history-repository.test.ts +++ b/src/backend/tests/database/repositories/command-history-repository.test.ts @@ -17,33 +17,11 @@ describe("CommandHistoryRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE command_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - command TEXT NOT NULL, - executed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-1', 'two'), (3, 'user-2', 'other'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-1', 'two', '10.0.0.1', 22, 'root', 'password'), (3, 'user-2', 'other', '10.0.0.1', 22, 'root', 'password'); `); return new CommandHistoryRepository(context, onWrite); diff --git a/src/backend/tests/database/repositories/credential-sidebar-preference-repository.test.ts b/src/backend/tests/database/repositories/credential-sidebar-preference-repository.test.ts new file mode 100644 index 0000000..97914e9 --- /dev/null +++ b/src/backend/tests/database/repositories/credential-sidebar-preference-repository.test.ts @@ -0,0 +1,89 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { CredentialSidebarPreferenceRepository } from "../../../database/repositories/credential-sidebar-preference-repository.js"; + +describe("CredentialSidebarPreferenceRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO credential_sidebar_preferences (user_id, data, updated_at) + VALUES ( + 'user-1', + '{"version":1,"sort":{"key":"default","pinnedFirst":false}}', + '2026-01-01T00:00:00.000Z' + ); + `); + + return new CredentialSidebarPreferenceRepository(context, onWrite); + } + + it("finds a saved preferences row by user id", async () => { + const repo = await createRepository(); + + const existing = await repo.findByUserId("user-1"); + expect(existing?.data).toBe( + '{"version":1,"sort":{"key":"default","pinnedFirst":false}}', + ); + expect(await repo.findByUserId("user-2")).toBeNull(); + }); + + it("updates and inserts preferences with write notifications", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + const updated = await repo.upsert( + "user-1", + '{"version":1,"display":{"density":"compact"}}', + "2026-02-01T00:00:00.000Z", + ); + expect(updated).toMatchObject({ + userId: "user-1", + data: '{"version":1,"display":{"density":"compact"}}', + updatedAt: "2026-02-01T00:00:00.000Z", + }); + + const created = await repo.upsert( + "user-2", + '{"version":1,"sort":{"key":"manual"}}', + "2026-03-01T00:00:00.000Z", + ); + expect(created).toMatchObject({ + userId: "user-2", + data: '{"version":1,"sort":{"key":"manual"}}', + updatedAt: "2026-03-01T00:00:00.000Z", + }); + expect(writeCount).toBe(2); + }); + + it("deletes preferences for a user", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + await repo.upsert("user-2", '{"version":1}'); + + await expect(repo.deleteByUserId("user-1")).resolves.toBe(1); + await expect(repo.deleteByUserId("missing")).resolves.toBe(0); + + expect(await repo.findByUserId("user-1")).toBeNull(); + expect((await repo.findByUserId("user-2"))?.data).toBe('{"version":1}'); + expect(writeCount).toBe(2); + }); +}); diff --git a/src/backend/tests/database/repositories/dashboard-service-link-repository.test.ts b/src/backend/tests/database/repositories/dashboard-service-link-repository.test.ts index e3bf7a6..4f3c47c 100644 --- a/src/backend/tests/database/repositories/dashboard-service-link-repository.test.ts +++ b/src/backend/tests/database/repositories/dashboard-service-link-repository.test.ts @@ -17,24 +17,7 @@ describe("DashboardServiceLinkRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE dashboard_service_links ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - label TEXT NOT NULL, - url TEXT NOT NULL, - "order" INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); `); @@ -99,8 +82,10 @@ describe("DashboardServiceLinkRepository", () => { ); expect(writeCount).toBe(2); - expect(await repo.deleteForUser("user-2", link.id)).toBe(false); - expect(await repo.deleteForUser("user-1", link.id)).toBe(true); + expect(await repo.deleteForUser("user-2", link.id)).toBeNull(); + expect(await repo.deleteForUser("user-1", link.id)).toEqual({ + syncId: expect.any(String), + }); expect(writeCount).toBe(3); }); diff --git a/src/backend/tests/database/repositories/dismissed-alert-repository.test.ts b/src/backend/tests/database/repositories/dismissed-alert-repository.test.ts index 9cbff30..abc488b 100644 --- a/src/backend/tests/database/repositories/dismissed-alert-repository.test.ts +++ b/src/backend/tests/database/repositories/dismissed-alert-repository.test.ts @@ -17,22 +17,7 @@ describe("DismissedAlertRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE dismissed_alerts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - alert_id TEXT NOT NULL, - dismissed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); `); diff --git a/src/backend/tests/database/repositories/factory-context.test.ts b/src/backend/tests/database/repositories/factory-context.test.ts new file mode 100644 index 0000000..f2486cd --- /dev/null +++ b/src/backend/tests/database/repositories/factory-context.test.ts @@ -0,0 +1,88 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { DATABASE_DIALECT_ENV } from "../../../database/db/dialect.js"; + +// getDb() throws unless a database was initialized; the context's handle is +// not what this file is about. +vi.mock("../../../database/db/index.js", () => ({ + getDb: () => ({}), + getSqlite: () => ({}), + DatabaseSaveTrigger: { forceSave: vi.fn(), triggerSave: vi.fn() }, +})); + +const { + createCurrentRepositoryContext, + createCurrentRepositoryWriteHook, + createCurrentRepositoryLazyWriteHook, +} = await import("../../../database/repositories/factory.js"); + +// Neither cross-dialect harness reaches this function: both +// tests/database/repositories/test-support.ts and scripts/verify-dialects.mjs +// construct a DatabaseContext of their own. That is why the production path +// could report "sqlite" while connected to MySQL with CI green on all three +// engines, and why this asserts on the real factory rather than a fixture. +describe("createCurrentRepositoryContext", () => { + const saved = process.env[DATABASE_DIALECT_ENV]; + + beforeEach(() => { + delete process.env[DATABASE_DIALECT_ENV]; + }); + + afterEach(() => { + if (saved === undefined) delete process.env[DATABASE_DIALECT_ENV]; + else process.env[DATABASE_DIALECT_ENV] = saved; + }); + + it("defaults to sqlite when nothing is configured", () => { + expect(createCurrentRepositoryContext().dialect).toBe("sqlite"); + }); + + it("reports the configured dialect", () => { + for (const dialect of ["sqlite", "postgres", "mysql"]) { + process.env[DATABASE_DIALECT_ENV] = dialect; + expect(createCurrentRepositoryContext().dialect).toBe(dialect); + } + }); + + it("rejects an unsupported dialect rather than falling back to sqlite", () => { + process.env[DATABASE_DIALECT_ENV] = "oracle"; + expect(() => createCurrentRepositoryContext()).toThrow(/oracle/); + }); +}); + +describe("createCurrentRepositoryWriteHook", () => { + const saved = process.env[DATABASE_DIALECT_ENV]; + + afterEach(() => { + if (saved === undefined) delete process.env[DATABASE_DIALECT_ENV]; + else process.env[DATABASE_DIALECT_ENV] = saved; + }); + + it("installs a persist hook only for sqlite", () => { + process.env[DATABASE_DIALECT_ENV] = "sqlite"; + expect(createCurrentRepositoryWriteHook("test")).toBeTypeOf("function"); + + for (const dialect of ["postgres", "mysql"]) { + process.env[DATABASE_DIALECT_ENV] = dialect; + expect(createCurrentRepositoryWriteHook("test")).toBeUndefined(); + } + }); +}); + +describe("createCurrentRepositoryLazyWriteHook", () => { + const saved = process.env[DATABASE_DIALECT_ENV]; + + afterEach(() => { + if (saved === undefined) delete process.env[DATABASE_DIALECT_ENV]; + else process.env[DATABASE_DIALECT_ENV] = saved; + }); + + it("installs a debounced persist hook only for sqlite", () => { + process.env[DATABASE_DIALECT_ENV] = "sqlite"; + expect(createCurrentRepositoryLazyWriteHook("test")).toBeTypeOf("function"); + + for (const dialect of ["postgres", "mysql"]) { + process.env[DATABASE_DIALECT_ENV] = dialect; + expect(createCurrentRepositoryLazyWriteHook("test")).toBeUndefined(); + } + }); +}); diff --git a/src/backend/tests/database/repositories/field-encryption-boundary.test.ts b/src/backend/tests/database/repositories/field-encryption-boundary.test.ts deleted file mode 100644 index a7dd89d..0000000 --- a/src/backend/tests/database/repositories/field-encryption-boundary.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import crypto from "crypto"; -import { describe, expect, it } from "vitest"; -import { FieldEncryptionBoundary } from "../../../database/repositories/field-encryption-boundary.js"; - -describe("FieldEncryptionBoundary", () => { - const userDataKey = crypto.randomBytes(32); - - it("encrypts sensitive host fields while leaving queryable metadata plaintext", () => { - const host = { - id: 42, - userId: "user-1", - name: "prod-db", - ip: "10.0.0.5", - username: "root", - password: "secret", - rdpPassword: "rdp-secret", - }; - - const encrypted = FieldEncryptionBoundary.encryptRecord( - "ssh_data", - host, - userDataKey, - ); - - expect(encrypted.password).not.toBe("secret"); - expect(encrypted.rdpPassword).not.toBe("rdp-secret"); - expect(encrypted.ip).toBe("10.0.0.5"); - expect(encrypted.name).toBe("prod-db"); - - const decrypted = FieldEncryptionBoundary.decryptRecord( - "ssh_data", - encrypted, - userDataKey, - ); - expect(decrypted).toMatchObject(host); - }); - - it("encrypts credential secret fields and keeps metadata plaintext", () => { - const credential = { - id: 7, - userId: "user-1", - name: "primary credential", - authType: "key", - key: "private-key-material", - keyPassword: "key-password", - }; - - const encrypted = FieldEncryptionBoundary.encryptRecord( - "ssh_credentials", - credential, - userDataKey, - ); - - expect(encrypted.key).not.toBe("private-key-material"); - expect(encrypted.keyPassword).not.toBe("key-password"); - expect(encrypted.name).toBe("primary credential"); - - expect( - FieldEncryptionBoundary.decryptRecord( - "ssh_credentials", - encrypted, - userDataKey, - ), - ).toMatchObject(credential); - }); - - it("keeps empty and non-string sensitive values unchanged", () => { - const encrypted = FieldEncryptionBoundary.encryptRecord( - "ssh_data", - { - id: 1, - password: "", - key: null, - }, - userDataKey, - ); - - expect(encrypted.password).toBe(""); - expect(encrypted.key).toBeNull(); - }); - - it("requires a stable record id instead of inventing a temporary encryption context", () => { - expect(() => - FieldEncryptionBoundary.encryptRecord( - "ssh_data", - { password: "secret" }, - userDataKey, - ), - ).toThrow(/stable record id/); - }); - - it("classifies sensitive, plaintext, and unknown fields", () => { - expect(FieldEncryptionBoundary.classifyField("ssh_data", "password")).toBe( - "sensitive", - ); - expect(FieldEncryptionBoundary.classifyField("ssh_data", "ip")).toBe( - "plaintext", - ); - expect(FieldEncryptionBoundary.classifyField("ssh_data", "newField")).toBe( - "unknown", - ); - }); -}); diff --git a/src/backend/tests/database/repositories/file-manager-bookmark-repository.test.ts b/src/backend/tests/database/repositories/file-manager-bookmark-repository.test.ts index 5ae4405..71cf3df 100644 --- a/src/backend/tests/database/repositories/file-manager-bookmark-repository.test.ts +++ b/src/backend/tests/database/repositories/file-manager-bookmark-repository.test.ts @@ -17,50 +17,11 @@ describe("FileManagerBookmarkRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE file_manager_recent ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - last_opened TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE file_manager_pinned ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - pinned_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE file_manager_shortcuts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-1', 'two'), (3, 'user-2', 'other'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-1', 'two', '10.0.0.1', 22, 'root', 'password'), (3, 'user-2', 'other', '10.0.0.1', 22, 'root', 'password'); `); return new FileManagerBookmarkRepository(context, onWrite); diff --git a/src/backend/tests/database/repositories/fleet-repository.test.ts b/src/backend/tests/database/repositories/fleet-repository.test.ts new file mode 100644 index 0000000..03c3725 --- /dev/null +++ b/src/backend/tests/database/repositories/fleet-repository.test.ts @@ -0,0 +1,103 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { FleetRepository } from "../../../database/repositories/fleet-repository.js"; + +describe("FleetRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise<{ repository: FleetRepository }> { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type, tags) + VALUES + (1, 'user-1', 'web-1', '10.0.0.1', 22, 'root', 'password', 'prod-web,edge'), + (2, 'user-1', 'web-2', '10.0.0.2', 22, 'root', 'password', 'prod-web'), + (3, 'user-1', 'db-1', '10.0.0.3', 22, 'root', 'password', 'prod-db'), + (4, 'user-1', 'static-only', '10.0.0.4', 22, 'root', 'password', NULL), + (5, 'user-2', 'other-user-host', '10.0.0.5', 22, 'root', 'password', 'prod-web'); + `); + + return { repository: new FleetRepository(context, onWrite) }; + } + + it("unions static membership and tag-matched hosts, deduplicated by id", async () => { + let writes = 0; + const { repository } = await createRepository(() => { + writes += 1; + }); + + const fleet = await repository.create("user-1", { + name: "web fleet", + tagRules: ["prod-web"], + }); + // host 1 matches by tag AND is added statically - must appear once. + await repository.addMember(fleet.id, 1); + // host 4 has no tags - only reachable via static membership. + await repository.addMember(fleet.id, 4); + + const members = await repository.listEffectiveMembers("user-1", fleet.id); + const ids = members.map((m) => m.id).sort((a, b) => a - b); + + expect(ids).toEqual([1, 2, 4]); + expect(writes).toBeGreaterThan(0); + }); + + it("never returns another user's hosts even if tags match", async () => { + const { repository } = await createRepository(); + + const fleet = await repository.create("user-1", { + name: "web fleet", + tagRules: ["prod-web"], + }); + + const members = await repository.listEffectiveMembers("user-1", fleet.id); + expect(members.map((m) => m.id)).not.toContain(5); + }); + + it("returns only static members when a fleet has no tag rules", async () => { + const { repository } = await createRepository(); + + const fleet = await repository.create("user-1", { name: "static fleet" }); + await repository.addMember(fleet.id, 3); + + const members = await repository.listEffectiveMembers("user-1", fleet.id); + expect(members.map((m) => m.id)).toEqual([3]); + }); + + it("removeMember only drops the static row, not tag-based membership", async () => { + const { repository } = await createRepository(); + + const fleet = await repository.create("user-1", { + name: "web fleet", + tagRules: ["prod-web"], + }); + await repository.addMember(fleet.id, 1); + + await expect(repository.removeMember(fleet.id, 1)).resolves.toBe(true); + + // host 1 still matches the tag rule, so it remains an effective member. + const members = await repository.listEffectiveMembers("user-1", fleet.id); + expect(members.map((m) => m.id)).toContain(1); + }); + + it("returns an empty list for a fleet the caller does not own", async () => { + const { repository } = await createRepository(); + const fleet = await repository.create("user-1", { name: "private" }); + + await expect( + repository.listEffectiveMembers("user-2", fleet.id), + ).resolves.toEqual([]); + }); +}); diff --git a/src/backend/tests/database/repositories/homepage-item-repository.test.ts b/src/backend/tests/database/repositories/homepage-item-repository.test.ts index 09a5dc2..a81f7ea 100644 --- a/src/backend/tests/database/repositories/homepage-item-repository.test.ts +++ b/src/backend/tests/database/repositories/homepage-item-repository.test.ts @@ -17,24 +17,7 @@ describe("HomepageItemRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE homepage_items ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - type_id TEXT NOT NULL, - title TEXT, - config TEXT NOT NULL DEFAULT '{}', - folder_id INTEGER, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); `); @@ -107,8 +90,10 @@ describe("HomepageItemRepository", () => { ).toBeNull(); expect(writeCount).toBe(2); - expect(await repo.deleteForUser("user-2", item.id)).toBe(false); - expect(await repo.deleteForUser("user-1", item.id)).toBe(true); + expect(await repo.deleteForUser("user-2", item.id)).toBeNull(); + expect(await repo.deleteForUser("user-1", item.id)).toEqual({ + syncId: expect.any(String), + }); expect(writeCount).toBe(3); }); diff --git a/src/backend/tests/database/repositories/homepage-layout-repository.test.ts b/src/backend/tests/database/repositories/homepage-layout-repository.test.ts index 479ae06..56bd3b7 100644 --- a/src/backend/tests/database/repositories/homepage-layout-repository.test.ts +++ b/src/backend/tests/database/repositories/homepage-layout-repository.test.ts @@ -17,22 +17,7 @@ describe("HomepageLayoutRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE homepage_layouts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL UNIQUE, - layout TEXT NOT NULL DEFAULT '{}', - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); `); diff --git a/src/backend/tests/database/repositories/host-credential-repositories.test.ts b/src/backend/tests/database/repositories/host-credential-repositories.test.ts index 3973618..fd7ec3f 100644 --- a/src/backend/tests/database/repositories/host-credential-repositories.test.ts +++ b/src/backend/tests/database/repositories/host-credential-repositories.test.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { afterEach, describe, expect, it, vi } from "vitest"; import { TestSqliteDatabase } from "./test-support.js"; import { CredentialRepository } from "../../../database/repositories/credential-repository.js"; @@ -21,170 +22,10 @@ describe("HostRepository and CredentialRepository", () => { ): Promise<{ credentials: CredentialRepository; hosts: HostRepository; - sqlite: NonNullable< - Awaited>["sqlite"] - >; }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE ssh_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - folder TEXT, - tags TEXT, - auth_type TEXT NOT NULL, - username TEXT, - password TEXT, - key TEXT, - private_key TEXT, - public_key TEXT, - key_password TEXT, - key_type TEXT, - detected_key_type TEXT, - cert_public_key TEXT, - usage_count INTEGER NOT NULL DEFAULT 0, - last_used TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - connection_type TEXT NOT NULL DEFAULT 'ssh', - name TEXT, - ip TEXT NOT NULL, - port INTEGER NOT NULL, - username TEXT NOT NULL, - folder TEXT, - tags TEXT, - pin INTEGER NOT NULL DEFAULT 0, - auth_type TEXT NOT NULL, - use_warpgate INTEGER NOT NULL DEFAULT 0, - force_keyboard_interactive TEXT, - password TEXT, - key TEXT, - key_password TEXT, - key_type TEXT, - sudo_password TEXT, - autostart_password TEXT, - autostart_key TEXT, - autostart_key_password TEXT, - credential_id INTEGER, - override_credential_username INTEGER, - vault_profile_id INTEGER, - enable_terminal INTEGER NOT NULL DEFAULT 1, - enable_session_logging INTEGER NOT NULL DEFAULT 1, - enable_command_history INTEGER NOT NULL DEFAULT 1, - enable_tunnel INTEGER NOT NULL DEFAULT 1, - tunnel_connections TEXT, - jump_hosts TEXT, - enable_file_manager INTEGER NOT NULL DEFAULT 1, - scp_legacy INTEGER NOT NULL DEFAULT 0, - enable_docker INTEGER NOT NULL DEFAULT 0, - enable_tmux_monitor INTEGER NOT NULL DEFAULT 0, - show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1, - show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0, - default_path TEXT, - stats_config TEXT, - docker_config TEXT, - enable_proxmox INTEGER NOT NULL DEFAULT 0, - proxmox_config TEXT, - terminal_config TEXT, - quick_actions TEXT, - notes TEXT, - enable_ssh INTEGER NOT NULL DEFAULT 1, - enable_rdp INTEGER NOT NULL DEFAULT 0, - enable_vnc INTEGER NOT NULL DEFAULT 0, - enable_telnet INTEGER NOT NULL DEFAULT 0, - ssh_port INTEGER DEFAULT 22, - rdp_port INTEGER DEFAULT 3389, - vnc_port INTEGER DEFAULT 5900, - telnet_port INTEGER DEFAULT 23, - rdp_credential_id INTEGER, - rdp_user TEXT, - rdp_password TEXT, - rdp_domain TEXT, - rdp_security TEXT, - rdp_ignore_cert INTEGER DEFAULT 0, - vnc_credential_id INTEGER, - vnc_password TEXT, - vnc_user TEXT, - telnet_user TEXT, - telnet_password TEXT, - telnet_credential_id INTEGER, - rdp_auth_type TEXT, - vnc_auth_type TEXT, - telnet_auth_type TEXT, - domain TEXT, - security TEXT, - ignore_cert INTEGER DEFAULT 0, - guacamole_config TEXT, - use_socks5 INTEGER, - socks5_host TEXT, - socks5_port INTEGER, - socks5_username TEXT, - socks5_password TEXT, - socks5_proxy_chain TEXT, - mac_address TEXT, - wol_broadcast_address TEXT, - port_knock_sequence TEXT, - host_key_fingerprint TEXT, - host_key_type TEXT, - host_key_algorithm TEXT DEFAULT 'sha256', - host_key_first_seen TEXT, - host_key_last_verified TEXT, - host_key_changed_count INTEGER DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (credential_id) REFERENCES ssh_credentials(id) ON DELETE SET NULL - ); - - CREATE TABLE host_access ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT, - role_id INTEGER, - granted_by TEXT NOT NULL, - permission_level TEXT NOT NULL DEFAULT 'view', - expires_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_accessed_at TEXT, - access_count INTEGER NOT NULL DEFAULT 0, - override_credential_id INTEGER, - FOREIGN KEY (host_id) REFERENCES ssh_data(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (override_credential_id) REFERENCES ssh_credentials(id) ON DELETE SET NULL - ); - - CREATE TABLE ssh_credential_usage ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - credential_id INTEGER NOT NULL, - host_id INTEGER NOT NULL, - user_id TEXT NOT NULL, - used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (credential_id) REFERENCES ssh_credentials(id) ON DELETE CASCADE, - FOREIGN KEY (host_id) REFERENCES ssh_data(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'user', 'hash'), ('user-2', 'other', 'hash'); @@ -193,7 +34,6 @@ describe("HostRepository and CredentialRepository", () => { return { credentials: new CredentialRepository(context, onCredentialWrite), hosts: new HostRepository(context, onHostWrite), - sqlite: context.sqlite!, }; } @@ -216,18 +56,27 @@ describe("HostRepository and CredentialRepository", () => { ).toBe("primary"); expect((await repo.credentials.findById(created.id))?.name).toBe("primary"); + // Backdate updated_at so the update's CURRENT_TIMESTAMP bump is + // deterministically observable regardless of clock resolution -- + // the sync engine's last-write-wins conflict resolution depends on + // every mutating update actually advancing this column. + await adapter!.run( + sql`UPDATE ssh_credentials SET updated_at = ${"2000-01-01 00:00:00"} WHERE id = ${created.id}`, + ); + const updated = await repo.credentials.updateForUser("user-1", created.id, { folder: "ops", tags: "linux,admin", }); expect(updated?.folder).toBe("ops"); + expect(updated?.updatedAt).not.toBe("2000-01-01 00:00:00"); expect( await repo.credentials.findByIdForUser("user-2", created.id), ).toBeNull(); - expect(await repo.credentials.deleteForUser("user-1", created.id)).toBe( - true, - ); + expect(await repo.credentials.deleteForUser("user-1", created.id)).toEqual({ + syncId: expect.any(String), + }); expect( await repo.credentials.findByIdForUser("user-1", created.id), ).toBeNull(); @@ -329,21 +178,30 @@ describe("HostRepository and CredentialRepository", () => { password: "secret", }); - const raw = repo.sqlite - .prepare("SELECT password FROM ssh_credentials WHERE id = ?") - .get(created.id) as { password: string }; + const raw = ( + await adapter!.query( + sql`SELECT password FROM ssh_credentials WHERE id = ${created.id}`, + ) + )[0] as { password: string }; expect(raw.password).toBe("user-encrypted-password"); + await adapter!.run( + sql`UPDATE ssh_credentials SET updated_at = ${"2000-01-01 00:00:00"} WHERE id = ${created.id}`, + ); + await repo.credentials.updateEncryptedForUser("user-1", created.id, { password: "updated-secret", }); - const updatedRaw = repo.sqlite - .prepare("SELECT password FROM ssh_credentials WHERE id = ?") - .get(created.id) as { password: string }; + const updatedRaw = ( + await adapter!.query( + sql`SELECT password, updated_at FROM ssh_credentials WHERE id = ${created.id}`, + ) + )[0] as { password: string; updated_at: string }; expect(updatedRaw.password).toBe("user-encrypted-password"); + expect(updatedRaw.updated_at).not.toBe("2000-01-01 00:00:00"); expect(DataCrypto.encryptRecord).toHaveBeenCalledWith( "ssh_credentials", expect.objectContaining({ password: "updated-secret" }), @@ -374,7 +232,7 @@ describe("HostRepository and CredentialRepository", () => { const onWrite = vi.fn(); const repo = await createRepositories(onWrite); - await repo.credentials.create({ + const primary = await repo.credentials.create({ userId: "user-1", name: "primary", authType: "password", @@ -392,6 +250,9 @@ describe("HostRepository and CredentialRepository", () => { authType: "password", folder: "prod", }); + await adapter!.run( + sql`UPDATE ssh_credentials SET updated_at = ${"2000-01-01 00:00:00"} WHERE id = ${primary.id}`, + ); onWrite.mockClear(); await expect( @@ -401,6 +262,13 @@ describe("HostRepository and CredentialRepository", () => { expect(await repo.credentials.listFolders("user-1")).toEqual(["ops"]); expect(await repo.credentials.listFolders("user-2")).toEqual(["prod"]); expect(onWrite).toHaveBeenCalledTimes(1); + + const renamedRow = ( + await adapter!.query( + sql`SELECT updated_at FROM ssh_credentials WHERE id = ${primary.id}`, + ) + )[0] as { updated_at: string }; + expect(renamedRow.updated_at).not.toBe("2000-01-01 00:00:00"); }); it("returns empty credential reads when user data is locked", async () => { @@ -441,14 +309,21 @@ describe("HostRepository and CredentialRepository", () => { (await repo.hosts.listByUserId("user-1")).map((item) => item.id), ).toEqual([host.id]); + await adapter!.run( + sql`UPDATE ssh_data SET updated_at = ${"2000-01-01 00:00:00"} WHERE id = ${host.id}`, + ); + const updated = await repo.hosts.updateForUser("user-1", host.id, { name: "web-1-renamed", folder: "prod", }); expect(updated?.name).toBe("web-1-renamed"); + expect(updated?.updatedAt).not.toBe("2000-01-01 00:00:00"); expect(await repo.hosts.findByIdForUser("user-2", host.id)).toBeNull(); - expect(await repo.hosts.deleteForUser("user-1", host.id)).toBe(true); + expect(await repo.hosts.deleteForUser("user-1", host.id)).toEqual({ + syncId: expect.any(String), + }); expect(await repo.hosts.findById(host.id)).toBeNull(); }); @@ -478,21 +353,30 @@ describe("HostRepository and CredentialRepository", () => { password: "secret", }); - const raw = repo.sqlite - .prepare("SELECT password FROM ssh_data WHERE id = ?") - .get(created.id) as { password: string }; + const raw = ( + await adapter!.query( + sql`SELECT password FROM ssh_data WHERE id = ${created.id}`, + ) + )[0] as { password: string }; expect(raw.password).toBe("encrypted-host-password"); + await adapter!.run( + sql`UPDATE ssh_data SET updated_at = ${"2000-01-01 00:00:00"} WHERE id = ${created.id}`, + ); + await repo.hosts.updateEncryptedForUser("user-1", created.id, { password: "updated-secret", }); - const updatedRaw = repo.sqlite - .prepare("SELECT password FROM ssh_data WHERE id = ?") - .get(created.id) as { password: string }; + const updatedRaw = ( + await adapter!.query( + sql`SELECT password, updated_at FROM ssh_data WHERE id = ${created.id}`, + ) + )[0] as { password: string; updated_at: string }; expect(updatedRaw.password).toBe("encrypted-host-password"); + expect(updatedRaw.updated_at).not.toBe("2000-01-01 00:00:00"); expect(DataCrypto.encryptRecord).toHaveBeenCalledWith( "ssh_data", expect.objectContaining({ password: "updated-secret" }), @@ -617,6 +501,9 @@ describe("HostRepository and CredentialRepository", () => { username: "root", authType: "password", }); + await adapter!.run( + sql`UPDATE ssh_data SET updated_at = ${"2000-01-01 00:00:00"} WHERE id IN (${first.id}, ${second.id})`, + ); onWrite.mockClear(); const states = await repo.hosts.listBulkUpdateState("user-1", [ @@ -634,6 +521,12 @@ describe("HostRepository and CredentialRepository", () => { expect((await repo.hosts.findById(first.id))?.folder).toBe("ops"); expect((await repo.hosts.findById(other.id))?.folder).toBeNull(); expect(onWrite).toHaveBeenCalledTimes(1); + expect((await repo.hosts.findById(first.id))?.updatedAt).not.toBe( + "2000-01-01 00:00:00", + ); + expect((await repo.hosts.findById(second.id))?.updatedAt).not.toBe( + "2000-01-01 00:00:00", + ); }); it("records credential usage and increments usage counters", async () => { @@ -668,6 +561,67 @@ describe("HostRepository and CredentialRepository", () => { expect(updated?.lastUsed).toBe("2026-06-26T00:00:00.000Z"); }); + it("sets a distinct sortOrder per credential via reorderForUser", async () => { + const onWrite = vi.fn(); + const repo = await createRepositories(onWrite); + + const first = await repo.credentials.create({ + userId: "user-1", + name: "one", + authType: "password", + }); + const second = await repo.credentials.create({ + userId: "user-1", + name: "two", + authType: "password", + }); + onWrite.mockClear(); + + const updated = await repo.credentials.reorderForUser("user-1", [ + { id: first.id, sortOrder: 2000 }, + { id: second.id, sortOrder: 1000 }, + ]); + expect(updated).toBe(2); + expect(onWrite).toHaveBeenCalledTimes(1); + + expect( + await adapter!.query( + sql`SELECT id, sort_order FROM ssh_credentials WHERE user_id = 'user-1' ORDER BY id`, + ), + ).toEqual([ + { id: first.id, sort_order: 2000 }, + { id: second.id, sort_order: 1000 }, + ]); + }); + + it("ignores credential ids the user does not own when reordering", async () => { + const repo = await createRepositories(); + + const other = await repo.credentials.create({ + userId: "user-2", + name: "other", + authType: "password", + }); + + const updated = await repo.credentials.reorderForUser("user-1", [ + { id: other.id, sortOrder: 5000 }, + ]); + expect(updated).toBe(0); + + expect( + await adapter!.query( + sql`SELECT sort_order FROM ssh_credentials WHERE id = ${other.id}`, + ), + ).toEqual([{ sort_order: null }]); + }); + + it("no-ops reorderForUser on an empty positions array", async () => { + const repo = await createRepositories(); + await expect(repo.credentials.reorderForUser("user-1", [])).resolves.toBe( + 0, + ); + }); + it("cleans host access before deleting a host", async () => { const repo = await createRepositories(); const host = await repo.hosts.create({ @@ -679,13 +633,13 @@ describe("HostRepository and CredentialRepository", () => { authType: "password", }); - repo.sqlite - .prepare( - "INSERT INTO host_access (host_id, user_id, granted_by) VALUES (?, ?, ?)", - ) - .run(host.id, "user-2", "user-1"); + await adapter!.run( + sql`INSERT INTO host_access (host_id, user_id, granted_by) VALUES (${host.id}, ${"user-2"}, ${"user-1"})`, + ); expect(await repo.hosts.deleteAccessForHost(host.id)).toBe(1); - expect(await repo.hosts.deleteForUser("user-1", host.id)).toBe(true); + expect(await repo.hosts.deleteForUser("user-1", host.id)).toEqual({ + syncId: expect.any(String), + }); }); }); diff --git a/src/backend/tests/database/repositories/host-folder-repository.test.ts b/src/backend/tests/database/repositories/host-folder-repository.test.ts index ca7df0c..99f04c8 100644 --- a/src/backend/tests/database/repositories/host-folder-repository.test.ts +++ b/src/backend/tests/database/repositories/host-folder-repository.test.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { afterEach, describe, expect, it } from "vitest"; import { TestSqliteDatabase } from "./test-support.js"; import { HostFolderRepository } from "../../../database/repositories/host-folder-repository.js"; @@ -14,147 +15,21 @@ describe("HostFolderRepository", () => { async function createRepository( onWrite?: () => void | Promise, - ): Promise<{ - repository: HostFolderRepository; - sqlite: NonNullable< - Awaited>["sqlite"] - >; - }> { + ): Promise<{ repository: HostFolderRepository }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE ssh_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - folder TEXT, - auth_type TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - connection_type TEXT NOT NULL DEFAULT 'ssh', - name TEXT, - ip TEXT NOT NULL, - port INTEGER NOT NULL, - username TEXT NOT NULL, - folder TEXT, - tags TEXT, - pin INTEGER NOT NULL DEFAULT 0, - auth_type TEXT NOT NULL, - use_warpgate INTEGER NOT NULL DEFAULT 0, - force_keyboard_interactive TEXT, - password TEXT, - key TEXT, - key_password TEXT, - key_type TEXT, - sudo_password TEXT, - autostart_password TEXT, - autostart_key TEXT, - autostart_key_password TEXT, - credential_id INTEGER, - override_credential_username INTEGER, - vault_profile_id INTEGER, - enable_terminal INTEGER NOT NULL DEFAULT 1, - enable_session_logging INTEGER NOT NULL DEFAULT 1, - enable_command_history INTEGER NOT NULL DEFAULT 1, - enable_tunnel INTEGER NOT NULL DEFAULT 1, - tunnel_connections TEXT, - jump_hosts TEXT, - enable_file_manager INTEGER NOT NULL DEFAULT 1, - scp_legacy INTEGER NOT NULL DEFAULT 0, - enable_docker INTEGER NOT NULL DEFAULT 0, - enable_tmux_monitor INTEGER NOT NULL DEFAULT 0, - show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1, - show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0, - default_path TEXT, - stats_config TEXT, - docker_config TEXT, - enable_proxmox INTEGER NOT NULL DEFAULT 0, - proxmox_config TEXT, - terminal_config TEXT, - quick_actions TEXT, - notes TEXT, - enable_ssh INTEGER NOT NULL DEFAULT 1, - enable_rdp INTEGER NOT NULL DEFAULT 0, - enable_vnc INTEGER NOT NULL DEFAULT 0, - enable_telnet INTEGER NOT NULL DEFAULT 0, - ssh_port INTEGER DEFAULT 22, - rdp_port INTEGER DEFAULT 3389, - vnc_port INTEGER DEFAULT 5900, - telnet_port INTEGER DEFAULT 23, - rdp_credential_id INTEGER, - rdp_user TEXT, - rdp_password TEXT, - rdp_domain TEXT, - rdp_security TEXT, - rdp_ignore_cert INTEGER DEFAULT 0, - vnc_credential_id INTEGER, - vnc_password TEXT, - vnc_user TEXT, - telnet_user TEXT, - telnet_password TEXT, - telnet_credential_id INTEGER, - rdp_auth_type TEXT, - vnc_auth_type TEXT, - telnet_auth_type TEXT, - domain TEXT, - security TEXT, - ignore_cert INTEGER DEFAULT 0, - guacamole_config TEXT, - use_socks5 INTEGER, - socks5_host TEXT, - socks5_port INTEGER, - socks5_username TEXT, - socks5_password TEXT, - socks5_proxy_chain TEXT, - mac_address TEXT, - wol_broadcast_address TEXT, - port_knock_sequence TEXT, - host_key_fingerprint TEXT, - host_key_type TEXT, - host_key_algorithm TEXT DEFAULT 'sha256', - host_key_first_seen TEXT, - host_key_last_verified TEXT, - host_key_changed_count INTEGER DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE ssh_folders ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - color TEXT, - icon TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO ssh_credentials (id, user_id, name, folder, auth_type, username) + VALUES (1, 'user-1', 'cred-one', 'prod', 'password', 'root'), + (2, 'user-1', 'cred-two', 'prod / api', 'password', 'root'), + (3, 'user-2', 'cred-other', 'prod', 'password', 'root'); INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, auth_type) VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'prod', 'password'), (2, 'user-1', 'two', '10.0.0.2', 22, 'root', 'prod / api', 'password'), (3, 'user-2', 'other', '10.0.0.3', 22, 'root', 'prod', 'password'); - INSERT INTO ssh_credentials (id, user_id, name, folder, auth_type) - VALUES - (1, 'user-1', 'cred-one', 'prod', 'password'), - (2, 'user-1', 'cred-two', 'prod / api', 'password'), - (3, 'user-2', 'cred-other', 'prod', 'password'); INSERT INTO ssh_folders (id, user_id, name, color, icon) VALUES (1, 'user-1', 'prod', '#111111', 'server'), @@ -162,15 +37,12 @@ describe("HostFolderRepository", () => { (3, 'user-2', 'prod', '#333333', 'user'); `); - return { - repository: new HostFolderRepository(context, onWrite), - sqlite: context.sqlite!, - }; + return { repository: new HostFolderRepository(context, onWrite) }; } it("renames folders across hosts, credentials, and folder records", async () => { let writes = 0; - const { repository, sqlite } = await createRepository(() => { + const { repository } = await createRepository(() => { writes += 1; }); @@ -183,22 +55,23 @@ describe("HostFolderRepository", () => { ), ).resolves.toEqual({ updatedHosts: 2, updatedCredentials: 2 }); + // Portable on purpose: the rename builds the child path with string + // concatenation, which is the one place a dialect difference shows up as + // wrong data rather than an error. expect( - sqlite - .prepare("SELECT folder FROM ssh_data WHERE user_id = ? ORDER BY id") - .all("user-1"), + await adapter!.query( + sql`SELECT folder FROM ssh_data WHERE user_id = 'user-1' ORDER BY id`, + ), ).toEqual([{ folder: "ops" }, { folder: "ops / api" }]); expect( - sqlite - .prepare( - "SELECT folder FROM ssh_credentials WHERE user_id = ? ORDER BY id", - ) - .all("user-1"), + await adapter!.query( + sql`SELECT folder FROM ssh_credentials WHERE user_id = 'user-1' ORDER BY id`, + ), ).toEqual([{ folder: "ops" }, { folder: "ops / api" }]); expect( - sqlite - .prepare("SELECT name FROM ssh_folders WHERE user_id = ? ORDER BY id") - .all("user-1"), + await adapter!.query( + sql`SELECT name FROM ssh_folders WHERE user_id = 'user-1' ORDER BY id`, + ), ).toEqual([{ name: "ops" }, { name: "ops / api" }]); expect(writes).toBe(1); }); @@ -216,6 +89,7 @@ describe("HostFolderRepository", () => { "prod", "#abcdef", "folder", + undefined, "2026-02-01T00:00:00.000Z", ), ).resolves.toMatchObject({ @@ -228,6 +102,7 @@ describe("HostFolderRepository", () => { "new", null, null, + null, "2026-03-01T00:00:00.000Z", ), ).resolves.toMatchObject({ @@ -237,9 +112,31 @@ describe("HostFolderRepository", () => { expect(writes).toBe(2); }); + it("assigns a credential to a folder and resolves it for nested paths", async () => { + const { repository } = await createRepository(); + + await expect( + repository.upsertMetadata( + "user-1", + "prod", + undefined, + undefined, + 1, + "2026-02-01T00:00:00.000Z", + ), + ).resolves.toMatchObject({ + created: false, + folder: { credentialId: 1 }, + }); + + const folders = await repository.listFolders("user-1"); + const prodFolder = folders.find((f) => f.name === "prod"); + expect(prodFolder?.credentialId).toBe(1); + }); + it("lists and deletes hosts and folder records in a folder tree", async () => { let writes = 0; - const { repository, sqlite } = await createRepository(() => { + const { repository } = await createRepository(() => { writes += 1; }); @@ -248,29 +145,78 @@ describe("HostFolderRepository", () => { await repository.deleteHostsAndFolderRecords("user-1", "prod"); - expect(sqlite.prepare("SELECT id FROM ssh_data ORDER BY id").all()).toEqual( - [{ id: 3 }], - ); expect( - sqlite.prepare("SELECT id FROM ssh_folders ORDER BY id").all(), + await adapter!.query(sql`SELECT id FROM ssh_data ORDER BY id`), + ).toEqual([{ id: 3 }]); + expect( + await adapter!.query(sql`SELECT id FROM ssh_folders ORDER BY id`), ).toEqual([{ id: 3 }]); expect(writes).toBe(1); }); it("deletes folder records for a user", async () => { let writes = 0; - const { repository, sqlite } = await createRepository(() => { + const { repository } = await createRepository(() => { writes += 1; }); await expect(repository.deleteByUserId("user-1")).resolves.toBe(2); - expect(sqlite.prepare("SELECT id FROM ssh_data ORDER BY id").all()).toEqual( - [{ id: 1 }, { id: 2 }, { id: 3 }], - ); expect( - sqlite.prepare("SELECT id FROM ssh_folders ORDER BY id").all(), + await adapter!.query(sql`SELECT id FROM ssh_data ORDER BY id`), + ).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); + expect( + await adapter!.query(sql`SELECT id FROM ssh_folders ORDER BY id`), ).toEqual([{ id: 3 }]); expect(writes).toBe(1); }); + + it("sets sortOrder on existing folder rows", async () => { + let writes = 0; + const { repository } = await createRepository(() => { + writes += 1; + }); + + const updated = await repository.reorderFolders( + "user-1", + [ + { name: "prod", sortOrder: 2000 }, + { name: "prod / api", sortOrder: 1000 }, + ], + "2026-04-01T00:00:00.000Z", + ); + expect(updated).toBe(2); + expect(writes).toBe(1); + + expect( + await adapter!.query( + sql`SELECT name, sort_order FROM ssh_folders WHERE user_id = 'user-1' ORDER BY id`, + ), + ).toEqual([ + { name: "prod", sort_order: 2000 }, + { name: "prod / api", sort_order: 1000 }, + ]); + }); + + it("creates a folder row when reordering a folder with no existing metadata", async () => { + const { repository } = await createRepository(); + + const updated = await repository.reorderFolders( + "user-1", + [{ name: "implicit-folder", sortOrder: 500 }], + "2026-04-01T00:00:00.000Z", + ); + expect(updated).toBe(1); + + expect( + await adapter!.query( + sql`SELECT name, sort_order FROM ssh_folders WHERE name = 'implicit-folder'`, + ), + ).toEqual([{ name: "implicit-folder", sort_order: 500 }]); + }); + + it("no-ops on an empty positions array", async () => { + const { repository } = await createRepository(); + await expect(repository.reorderFolders("user-1", [])).resolves.toBe(0); + }); }); diff --git a/src/backend/tests/database/repositories/host-health-repository.test.ts b/src/backend/tests/database/repositories/host-health-repository.test.ts index 29ff31f..99058e3 100644 --- a/src/backend/tests/database/repositories/host-health-repository.test.ts +++ b/src/backend/tests/database/repositories/host-health-repository.test.ts @@ -17,44 +17,11 @@ describe("HostHealthRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE host_health_checks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - checks TEXT NOT NULL, - interval_seconds INTEGER NOT NULL DEFAULT 300, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE host_health_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - check_id TEXT NOT NULL, - ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - ok INTEGER NOT NULL, - latency_ms INTEGER, - detail TEXT - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-2', 'two'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '10.0.0.1', 22, 'root', 'password'); INSERT INTO host_health_checks ( user_id, host_id, checks, interval_seconds, created_at, updated_at ) diff --git a/src/backend/tests/database/repositories/host-metrics-history-repository.test.ts b/src/backend/tests/database/repositories/host-metrics-history-repository.test.ts index f20f5c4..3fb64e6 100644 --- a/src/backend/tests/database/repositories/host-metrics-history-repository.test.ts +++ b/src/backend/tests/database/repositories/host-metrics-history-repository.test.ts @@ -17,26 +17,13 @@ describe("HostMetricsHistoryRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) VALUES + ('user-1', 'user-1', 'hash'), + ('user-2', 'user-2', 'hash'); - CREATE TABLE host_metrics_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - cpu_percent REAL, - mem_percent REAL, - disk_percent REAL, - net_rx_bytes INTEGER, - net_tx_bytes INTEGER - ); - - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-2', 'two'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '10.0.0.1', 22, 'root', 'password'); INSERT INTO host_metrics_history ( host_id, ts, cpu_percent, mem_percent, disk_percent, net_rx_bytes, net_tx_bytes ) @@ -78,7 +65,7 @@ describe("HostMetricsHistoryRepository", () => { it("prunes old history for a host only", async () => { const repo = await createRepository(); - repo.pruneOlderThan(1, 1); + await repo.pruneOlderThan(1, 1); const rows = await repo.listRange( 1, diff --git a/src/backend/tests/database/repositories/host-metrics-preference-repository.test.ts b/src/backend/tests/database/repositories/host-metrics-preference-repository.test.ts index cc1ceeb..e45cb6e 100644 --- a/src/backend/tests/database/repositories/host-metrics-preference-repository.test.ts +++ b/src/backend/tests/database/repositories/host-metrics-preference-repository.test.ts @@ -17,33 +17,11 @@ describe("HostMetricsPreferenceRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - stats_config TEXT - ); - - CREATE TABLE host_metrics_preferences ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - layout TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO ssh_data (id, user_id, name, stats_config) - VALUES (1, 'user-1', 'one', '{}'), (2, 'user-2', 'two', '{}'); + INSERT INTO ssh_data (id, user_id, name, stats_config, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '{}', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '{}', '10.0.0.1', 22, 'root', 'password'); INSERT INTO host_metrics_preferences ( user_id, host_id, layout, created_at, updated_at ) diff --git a/src/backend/tests/database/repositories/host-repository.test.ts b/src/backend/tests/database/repositories/host-repository.test.ts new file mode 100644 index 0000000..468c481 --- /dev/null +++ b/src/backend/tests/database/repositories/host-repository.test.ts @@ -0,0 +1,74 @@ +import { sql } from "drizzle-orm"; +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { HostRepository } from "../../../database/repositories/host-repository.js"; + +describe("HostRepository.reorderForUser", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES + (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), + (2, 'user-1', 'two', '10.0.0.2', 22, 'root', 'password'), + (3, 'user-2', 'other', '10.0.0.3', 22, 'root', 'password'); + `); + + return new HostRepository(context, onWrite); + } + + it("sets a distinct sortOrder per host", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + const updated = await repo.reorderForUser("user-1", [ + { id: 1, sortOrder: 2000 }, + { id: 2, sortOrder: 1000 }, + ]); + expect(updated).toBe(2); + expect(writeCount).toBe(1); + + expect( + await adapter!.query( + sql`SELECT id, sort_order FROM ssh_data WHERE user_id = 'user-1' ORDER BY id`, + ), + ).toEqual([ + { id: 1, sort_order: 2000 }, + { id: 2, sort_order: 1000 }, + ]); + }); + + it("ignores ids the user does not own", async () => { + const repo = await createRepository(); + + const updated = await repo.reorderForUser("user-1", [ + { id: 3, sortOrder: 5000 }, + ]); + expect(updated).toBe(0); + + expect( + await adapter!.query(sql`SELECT sort_order FROM ssh_data WHERE id = 3`), + ).toEqual([{ sort_order: null }]); + }); + + it("no-ops on an empty positions array", async () => { + const repo = await createRepository(); + await expect(repo.reorderForUser("user-1", [])).resolves.toBe(0); + }); +}); diff --git a/src/backend/tests/database/repositories/host-resolution-repository.test.ts b/src/backend/tests/database/repositories/host-resolution-repository.test.ts index e90eb57..4ef296d 100644 --- a/src/backend/tests/database/repositories/host-resolution-repository.test.ts +++ b/src/backend/tests/database/repositories/host-resolution-repository.test.ts @@ -27,146 +27,15 @@ describe("HostResolutionRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - connection_type TEXT NOT NULL DEFAULT 'ssh', - name TEXT, - ip TEXT NOT NULL, - port INTEGER NOT NULL, - username TEXT NOT NULL, - folder TEXT, - tags TEXT, - pin INTEGER NOT NULL DEFAULT 0, - auth_type TEXT NOT NULL, - use_warpgate INTEGER NOT NULL DEFAULT 0, - force_keyboard_interactive TEXT, - password TEXT, - key TEXT, - key_password TEXT, - key_type TEXT, - sudo_password TEXT, - autostart_password TEXT, - autostart_key TEXT, - autostart_key_password TEXT, - credential_id INTEGER, - override_credential_username INTEGER, - vault_profile_id INTEGER, - enable_terminal INTEGER NOT NULL DEFAULT 1, - enable_session_logging INTEGER NOT NULL DEFAULT 1, - enable_command_history INTEGER NOT NULL DEFAULT 1, - enable_tunnel INTEGER NOT NULL DEFAULT 1, - tunnel_connections TEXT, - jump_hosts TEXT, - enable_file_manager INTEGER NOT NULL DEFAULT 1, - scp_legacy INTEGER NOT NULL DEFAULT 0, - enable_docker INTEGER NOT NULL DEFAULT 0, - enable_tmux_monitor INTEGER NOT NULL DEFAULT 0, - show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1, - show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0, - default_path TEXT, - stats_config TEXT, - docker_config TEXT, - enable_proxmox INTEGER NOT NULL DEFAULT 0, - proxmox_config TEXT, - terminal_config TEXT, - quick_actions TEXT, - notes TEXT, - enable_ssh INTEGER NOT NULL DEFAULT 1, - enable_rdp INTEGER NOT NULL DEFAULT 0, - enable_vnc INTEGER NOT NULL DEFAULT 0, - enable_telnet INTEGER NOT NULL DEFAULT 0, - ssh_port INTEGER DEFAULT 22, - rdp_port INTEGER DEFAULT 3389, - vnc_port INTEGER DEFAULT 5900, - telnet_port INTEGER DEFAULT 23, - rdp_credential_id INTEGER, - rdp_user TEXT, - rdp_password TEXT, - rdp_domain TEXT, - rdp_security TEXT, - rdp_ignore_cert INTEGER DEFAULT 0, - vnc_credential_id INTEGER, - vnc_password TEXT, - vnc_user TEXT, - telnet_user TEXT, - telnet_password TEXT, - telnet_credential_id INTEGER, - rdp_auth_type TEXT, - vnc_auth_type TEXT, - telnet_auth_type TEXT, - domain TEXT, - security TEXT, - ignore_cert INTEGER DEFAULT 0, - guacamole_config TEXT, - use_socks5 INTEGER, - socks5_host TEXT, - socks5_port INTEGER, - socks5_username TEXT, - socks5_password TEXT, - socks5_proxy_chain TEXT, - mac_address TEXT, - wol_broadcast_address TEXT, - port_knock_sequence TEXT, - host_key_fingerprint TEXT, - host_key_type TEXT, - host_key_algorithm TEXT DEFAULT 'sha256', - host_key_first_seen TEXT, - host_key_last_verified TEXT, - host_key_changed_count INTEGER DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE ssh_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - folder TEXT, - tags TEXT, - auth_type TEXT NOT NULL, - username TEXT, - password TEXT, - key TEXT, - private_key TEXT, - public_key TEXT, - key_password TEXT, - key_type TEXT, - detected_key_type TEXT, - cert_public_key TEXT, - usage_count INTEGER NOT NULL DEFAULT 0, - last_used TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE host_access ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT, - role_id INTEGER, - granted_by TEXT NOT NULL, - permission_level TEXT NOT NULL DEFAULT 'view', - expires_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_accessed_at TEXT, - access_count INTEGER NOT NULL DEFAULT 0, - override_credential_id INTEGER - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO ssh_credentials ( + id, user_id, name, auth_type, username, password, private_key, key_password + ) + VALUES + (7, 'user-1', 'owner', 'password', 'root', 'secret', NULL, NULL), + (8, 'user-2', 'override', 'key', 'alice', NULL, 'private', 'pass'); INSERT INTO ssh_data ( id, user_id, name, ip, port, username, auth_type, credential_id, tunnel_connections @@ -175,16 +44,15 @@ describe("HostResolutionRepository", () => { (1, 'user-1', 'web', '10.0.0.1', 22, 'root', 'password', 7, '[{"autoStart":true}]'), (2, 'user-1', 'db', '10.0.0.2', 22, 'admin', 'none', NULL, NULL), (3, 'user-2', 'other', '10.0.0.3', 22, 'root', 'none', NULL, '[{"autoStart":false}]'); - INSERT INTO ssh_credentials ( - id, user_id, name, auth_type, username, password, private_key, key_password - ) + INSERT INTO ssh_folders (user_id, name, credential_id) VALUES - (7, 'user-1', 'owner', 'password', 'root', 'secret', NULL, NULL), - (8, 'user-2', 'override', 'key', 'alice', NULL, 'private', 'pass'); + ('user-1', 'switches', 7), + ('user-1', 'switches / floor1', NULL), + ('user-1', 'no-cred', NULL); INSERT INTO host_access ( - host_id, user_id, granted_by, permission_level, override_credential_id + host_id, user_id, granted_by, permission_level ) - VALUES (1, 'user-2', 'user-1', 'execute', 8); + VALUES (1, 'user-2', 'user-1', 'execute'); `); return new HostResolutionRepository(context, onWrite); @@ -276,7 +144,12 @@ describe("HostResolutionRepository", () => { const repository = await createRepository(); const rows = await repository.listHostRowsForAccessList("user-2", [ - { hostId: 1, permissionLevel: "execute", expiresAt: null }, + { hostId: 1, permissionLevel: "view", expiresAt: null }, + { + hostId: 1, + permissionLevel: "manage", + expiresAt: "2026-07-01T00:00:00.000Z", + }, { hostId: 3, permissionLevel: "view", expiresAt: null }, { hostId: 999, permissionLevel: "view", expiresAt: null }, ]); @@ -295,8 +168,8 @@ describe("HostResolutionRepository", () => { userId: "user-1", ownerId: "user-1", isShared: true, - permissionLevel: "execute", - expiresAt: null, + permissionLevel: "manage", + expiresAt: "2026-07-01T00:00:00.000Z", }); expect(DataCrypto.decryptRecord).not.toHaveBeenCalled(); }); @@ -324,6 +197,8 @@ describe("HostResolutionRepository", () => { telnetCredentialId: null, vaultProfileId: null, authType: "password", + parentHostId: null, + folder: null, }); await expect(repository.findHostUpdateState(999)).resolves.toBeNull(); expect(DataCrypto.decryptRecord).not.toHaveBeenCalled(); @@ -482,14 +357,91 @@ describe("HostResolutionRepository", () => { ).resolves.toBeNull(); }); - it("loads override credential ids for shared host resolution", async () => { + it("resolves a folder's assigned credential, walking up to parent folders", async () => { const repository = await createRepository(); await expect( - repository.findOverrideCredentialId(1, "user-2"), - ).resolves.toBe(8); + repository.findFolderCredentialId("user-1", "switches"), + ).resolves.toBe(7); await expect( - repository.findOverrideCredentialId(1, "user-1"), + repository.findFolderCredentialId("user-1", "switches / floor1"), + ).resolves.toBe(7); + await expect( + repository.findFolderCredentialId("user-1", "no-cred"), + ).resolves.toBeNull(); + await expect( + repository.findFolderCredentialId("user-1", "unknown"), + ).resolves.toBeNull(); + await expect( + repository.findFolderCredentialId("user-1", ""), ).resolves.toBeNull(); }); + + describe("listCredentialsByIdsForUser", () => { + it("returns the owner's credentials keyed by id", async () => { + const repository = await createRepository(); + vi.mocked(DataCrypto.getUserDataKey).mockReturnValue( + Buffer.from("key") as never, + ); + + const byId = await repository.listCredentialsByIdsForUser([7], "user-1"); + + expect(byId.get(7)).toMatchObject({ id: 7, username: "root" }); + expect(DataCrypto.decryptRecord).toHaveBeenCalledWith( + "ssh_credentials", + expect.objectContaining({ id: 7 }), + "user-1", + expect.anything(), + ); + }); + + it("excludes credentials belonging to another user", async () => { + const repository = await createRepository(); + vi.mocked(DataCrypto.getUserDataKey).mockReturnValue( + Buffer.from("key") as never, + ); + + const byId = await repository.listCredentialsByIdsForUser( + [7, 8], + "user-1", + ); + + expect(byId.has(7)).toBe(true); + // 8 belongs to user-2 and must not leak into user-1's result. + expect(byId.has(8)).toBe(false); + }); + + it("issues no query and decrypts nothing for an empty id list", async () => { + const repository = await createRepository(); + + const byId = await repository.listCredentialsByIdsForUser([], "user-1"); + + expect(byId.size).toBe(0); + expect(DataCrypto.decryptRecord).not.toHaveBeenCalled(); + }); + + it("de-duplicates repeated ids so shared credentials decrypt once", async () => { + const repository = await createRepository(); + vi.mocked(DataCrypto.getUserDataKey).mockReturnValue( + Buffer.from("key") as never, + ); + + const byId = await repository.listCredentialsByIdsForUser( + [7, 7, 7], + "user-1", + ); + + expect(byId.size).toBe(1); + expect(DataCrypto.decryptRecord).toHaveBeenCalledTimes(1); + }); + + it("returns nothing when the user's data key is unavailable", async () => { + const repository = await createRepository(); + vi.mocked(DataCrypto.getUserDataKey).mockReturnValue(null as never); + + const byId = await repository.listCredentialsByIdsForUser([7], "user-1"); + + expect(byId.size).toBe(0); + }); + }); }); diff --git a/src/backend/tests/database/repositories/host-sidebar-preference-repository.test.ts b/src/backend/tests/database/repositories/host-sidebar-preference-repository.test.ts new file mode 100644 index 0000000..c19c87c --- /dev/null +++ b/src/backend/tests/database/repositories/host-sidebar-preference-repository.test.ts @@ -0,0 +1,89 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { HostSidebarPreferenceRepository } from "../../../database/repositories/host-sidebar-preference-repository.js"; + +describe("HostSidebarPreferenceRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO host_sidebar_preferences (user_id, data, updated_at) + VALUES ( + 'user-1', + '{"version":1,"sort":{"key":"default","pinnedFirst":false}}', + '2026-01-01T00:00:00.000Z' + ); + `); + + return new HostSidebarPreferenceRepository(context, onWrite); + } + + it("finds a saved preferences row by user id", async () => { + const repo = await createRepository(); + + const existing = await repo.findByUserId("user-1"); + expect(existing?.data).toBe( + '{"version":1,"sort":{"key":"default","pinnedFirst":false}}', + ); + expect(await repo.findByUserId("user-2")).toBeNull(); + }); + + it("updates and inserts preferences with write notifications", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + const updated = await repo.upsert( + "user-1", + '{"version":1,"groupKey":"tag"}', + "2026-02-01T00:00:00.000Z", + ); + expect(updated).toMatchObject({ + userId: "user-1", + data: '{"version":1,"groupKey":"tag"}', + updatedAt: "2026-02-01T00:00:00.000Z", + }); + + const created = await repo.upsert( + "user-2", + '{"version":1,"groupKey":"folder"}', + "2026-03-01T00:00:00.000Z", + ); + expect(created).toMatchObject({ + userId: "user-2", + data: '{"version":1,"groupKey":"folder"}', + updatedAt: "2026-03-01T00:00:00.000Z", + }); + expect(writeCount).toBe(2); + }); + + it("deletes preferences for a user", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + await repo.upsert("user-2", '{"version":1}'); + + await expect(repo.deleteByUserId("user-1")).resolves.toBe(1); + await expect(repo.deleteByUserId("missing")).resolves.toBe(0); + + expect(await repo.findByUserId("user-1")).toBeNull(); + expect((await repo.findByUserId("user-2"))?.data).toBe('{"version":1}'); + expect(writeCount).toBe(2); + }); +}); diff --git a/src/backend/tests/database/repositories/mutation-result.test.ts b/src/backend/tests/database/repositories/mutation-result.test.ts new file mode 100644 index 0000000..b905de2 --- /dev/null +++ b/src/backend/tests/database/repositories/mutation-result.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import { + insertedId, + rowsAffected, + supportsReturning, +} from "../../../database/repositories/mutation-result.js"; + +describe("rowsAffected", () => { + it("counts a returning() array from sqlite or postgres", () => { + expect(rowsAffected([{ id: 1 }, { id: 2 }, { id: 3 }])).toBe(3); + expect(rowsAffected([])).toBe(0); + }); + + it("reads affectedRows from a mysql write result", () => { + expect(rowsAffected({ affectedRows: 4, insertId: 0 })).toBe(4); + expect(rowsAffected({ affectedRows: 0 })).toBe(0); + }); + + it("reads changes from a better-sqlite3 write result", () => { + // The shape of a write with no .returning() attached โ€” verified against + // the driver, not assumed. + expect(rowsAffected({ changes: 1, lastInsertRowid: 7 })).toBe(1); + expect(rowsAffected({ changes: 0, lastInsertRowid: 7 })).toBe(0); + }); + + it("reads rowCount from a node-postgres write result", () => { + expect(rowsAffected({ rowCount: 3, rows: [], command: "DELETE" })).toBe(3); + }); + + it("unwraps the [header, fields] tuple mysql2 returns", () => { + expect(rowsAffected([{ affectedRows: 2 }, []])).toBe(2); + }); + + it("does not mistake a returning() array for a mysql header", () => { + // A single returned row is one row, not whatever affectedRows might say. + expect(rowsAffected([{ id: 7 }])).toBe(1); + }); + + it("reports zero for a shape it does not recognise", () => { + expect(rowsAffected(undefined)).toBe(0); + expect(rowsAffected(null)).toBe(0); + expect(rowsAffected({})).toBe(0); + }); +}); + +describe("insertedId", () => { + it("reads the id from a returning() array", () => { + expect(insertedId([{ id: 42 }])).toBe(42); + }); + + it("reads insertId from a mysql write result", () => { + expect(insertedId({ affectedRows: 1, insertId: 42 })).toBe(42); + expect(insertedId([{ affectedRows: 1, insertId: 42 }, []])).toBe(42); + }); + + it("treats mysql's zero insertId as absent", () => { + // MySQL reports 0 when the table has no autoincrement column. + expect(insertedId({ affectedRows: 1, insertId: 0 })).toBeNull(); + }); + + it("reads lastInsertRowid from better-sqlite3, as number or bigint", () => { + expect(insertedId({ changes: 1, lastInsertRowid: 9 })).toBe(9); + expect(insertedId({ changes: 1, lastInsertRowid: 9n })).toBe(9); + expect(insertedId({ changes: 1, lastInsertRowid: 0 })).toBeNull(); + }); + + it("returns null when nothing was inserted", () => { + expect(insertedId([])).toBeNull(); + expect(insertedId({})).toBeNull(); + expect(insertedId(undefined)).toBeNull(); + }); + + it("returns null for a non-numeric id", () => { + // Tables keyed by a text id, e.g. users. + expect(insertedId([{ id: "u-1" }])).toBeNull(); + }); +}); + +describe("supportsReturning", () => { + it("is false only for mysql", () => { + expect(supportsReturning("sqlite")).toBe(true); + expect(supportsReturning("postgres")).toBe(true); + // No RETURNING clause in MySQL, and drizzle's mysql-core does not expose + // the method โ€” call sites that need rows back must read first. + expect(supportsReturning("mysql")).toBe(false); + }); +}); diff --git a/src/backend/tests/database/repositories/network-topology-repository.test.ts b/src/backend/tests/database/repositories/network-topology-repository.test.ts index 1c8ad72..985336e 100644 --- a/src/backend/tests/database/repositories/network-topology-repository.test.ts +++ b/src/backend/tests/database/repositories/network-topology-repository.test.ts @@ -17,23 +17,7 @@ describe("NetworkTopologyRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE network_topology ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - topology TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); `); diff --git a/src/backend/tests/database/repositories/open-tab-repository.test.ts b/src/backend/tests/database/repositories/open-tab-repository.test.ts index 0d7f88c..abae491 100644 --- a/src/backend/tests/database/repositories/open-tab-repository.test.ts +++ b/src/backend/tests/database/repositories/open-tab-repository.test.ts @@ -17,29 +17,12 @@ describe("OpenTabRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE user_open_tabs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - tab_type TEXT NOT NULL, - host_id INTEGER, - label TEXT NOT NULL, - tab_order INTEGER NOT NULL DEFAULT 0, - backend_session_id TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) VALUES + (1, 'user-1', 'host-1', '10.0.0.1', 22, 'root', 'password'), + (2, 'user-1', 'host-2', '10.0.0.2', 22, 'root', 'password'); `); return new OpenTabRepository(context, onWrite); diff --git a/src/backend/tests/database/repositories/opkssh-token-repository.test.ts b/src/backend/tests/database/repositories/opkssh-token-repository.test.ts index 4700e21..de7efde 100644 --- a/src/backend/tests/database/repositories/opkssh-token-repository.test.ts +++ b/src/backend/tests/database/repositories/opkssh-token-repository.test.ts @@ -17,39 +17,11 @@ describe("OpksshTokenRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE opkssh_tokens ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - ssh_cert TEXT NOT NULL, - private_key TEXT NOT NULL, - email TEXT, - sub TEXT, - issuer TEXT, - audience TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - last_used TEXT, - UNIQUE(user_id, host_id) - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-1', 'two'), (3, 'user-2', 'other'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-1', 'two', '10.0.0.1', 22, 'root', 'password'), (3, 'user-2', 'other', '10.0.0.1', 22, 'root', 'password'); INSERT INTO opkssh_tokens ( user_id, host_id, ssh_cert, private_key, email, expires_at ) diff --git a/src/backend/tests/database/repositories/proxmox-node-history-repository.test.ts b/src/backend/tests/database/repositories/proxmox-node-history-repository.test.ts new file mode 100644 index 0000000..6e67701 --- /dev/null +++ b/src/backend/tests/database/repositories/proxmox-node-history-repository.test.ts @@ -0,0 +1,80 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { ProxmoxNodeHistoryRepository } from "../../../database/repositories/proxmox-node-history-repository.js"; + +describe("ProxmoxNodeHistoryRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) VALUES + ('user-1', 'user-1', 'hash'), + ('user-2', 'user-2', 'hash'); + + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'pve1', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'pve2', '10.0.0.2', 22, 'root', 'password'); + INSERT INTO proxmox_node_history ( + host_id, ts, cpu_percent, mem_percent, disk_percent, net_rx_bytes, net_tx_bytes + ) + VALUES + (1, '2026-01-01 00:00:00', 10, 20, 30, 100, 200), + (1, '2026-01-02 00:00:00', 11, 21, 31, 101, 201), + (1, '2999-01-01 00:00:00', 12, 22, 32, 102, 202), + (2, '2026-01-02 00:00:00', 99, 99, 99, 999, 999); + `); + + return new ProxmoxNodeHistoryRepository(context, onWrite); + } + + it("creates and lists node history rows by range", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + await repo.create({ + hostId: 1, + cpuPercent: 12, + memPercent: 22, + diskPercent: 32, + netRxBytes: 102, + netTxBytes: 202, + }); + + const rows = await repo.listRange( + 1, + "2026-01-01 00:00:00", + "2026-01-02 23:59:59", + ); + + expect(rows.map((row) => row.cpuPercent)).toEqual([10, 11]); + expect(writeCount).toBe(1); + }); + + it("prunes old history for a host only", async () => { + const repo = await createRepository(); + + await repo.pruneOlderThan(1, 1); + + const rows = await repo.listRange( + 1, + "2000-01-01 00:00:00", + "2999-12-31 23:59:59", + ); + expect(rows.map((row) => row.ts)).toEqual(["2999-01-01 00:00:00"]); + expect( + await repo.listRange(2, "2026-01-01 00:00:00", "2026-01-03 00:00:00"), + ).toHaveLength(1); + }); +}); diff --git a/src/backend/tests/database/repositories/rbac-access-repository.test.ts b/src/backend/tests/database/repositories/rbac-access-repository.test.ts index 1cc8e50..e292080 100644 --- a/src/backend/tests/database/repositories/rbac-access-repository.test.ts +++ b/src/backend/tests/database/repositories/rbac-access-repository.test.ts @@ -18,112 +18,30 @@ describe("RbacAccessRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - display_name TEXT NOT NULL, - description TEXT, - is_system INTEGER NOT NULL DEFAULT 0, - permissions TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE host_access ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT, - role_id INTEGER, - granted_by TEXT NOT NULL, - permission_level TEXT NOT NULL DEFAULT 'view', - expires_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_accessed_at TEXT, - access_count INTEGER NOT NULL DEFAULT 0, - override_credential_id INTEGER - ); - - CREATE TABLE shared_host_secrets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_access_id INTEGER NOT NULL, - target_user_id TEXT NOT NULL, - protocol TEXT NOT NULL DEFAULT 'ssh', - source_type TEXT NOT NULL DEFAULT 'credential', - original_credential_id INTEGER, - encrypted_username TEXT, - encrypted_auth_type TEXT, - encrypted_password TEXT, - encrypted_key TEXT, - encrypted_key_password TEXT, - encrypted_key_type TEXT, - encrypted_domain TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(host_access_id, target_user_id, protocol) - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT, - ip TEXT NOT NULL, - port INTEGER NOT NULL, - username TEXT NOT NULL, - credential_id INTEGER, - rdp_credential_id INTEGER, - vnc_credential_id INTEGER, - telnet_credential_id INTEGER, - folder TEXT, - tags TEXT - ); - - CREATE TABLE snippets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - content TEXT NOT NULL, - description TEXT, - folder TEXT, - "order" INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - host_filter TEXT - ); - - CREATE TABLE snippet_access ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - snippet_id INTEGER NOT NULL, - user_id TEXT, - role_id INTEGER, - granted_by TEXT NOT NULL, - permission_level TEXT NOT NULL DEFAULT 'view', - expires_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash, is_admin, is_oidc) VALUES ('admin', 'admin', 'hash', 1, 0), ('user-1', 'alice', 'hash', 0, 0), ('owner-1', 'owner', 'hash', 0, 0); - INSERT INTO roles (id, name, display_name, is_system) VALUES (7, 'ops', 'Operations', 0); - + INSERT INTO ssh_credentials (id, user_id, name, username, auth_type) VALUES + (123, 'admin', 'cred-123', 'root', 'password'), + (124, 'admin', 'cred-124', 'root', 'password'), + (125, 'admin', 'cred-125', 'root', 'password'), + (126, 'admin', 'cred-126', 'root', 'password'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) VALUES + (43, 'admin', 'host-43', '10.0.0.43', 22, 'root', 'password'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) VALUES + (44, 'admin', 'host-44', '10.0.0.45', 22, 'root', 'password'); INSERT INTO ssh_data ( - id, user_id, name, ip, port, username, credential_id, rdp_credential_id, vnc_credential_id, telnet_credential_id, folder, tags - ) - VALUES (42, 'owner-1', 'prod', '10.0.0.42', 22, 'root', 123, 124, 125, 126, 'servers', 'linux'); - + id, user_id, name, ip, port, username, credential_id, rdp_credential_id, vnc_credential_id, telnet_credential_id, folder, tags, auth_type) + VALUES (42, 'owner-1', 'prod', '10.0.0.42', 22, 'root', 123, 124, 125, 126, 'servers', 'linux', 'password'); + INSERT INTO snippets (id, user_id, name, content) + VALUES + (99, 'owner-1', 'deploy', 'echo deploy'), + (100, 'owner-1', 'rollback', 'echo rollback'); INSERT INTO host_access ( id, host_id, user_id, role_id, granted_by, permission_level, expires_at, created_at ) @@ -131,23 +49,18 @@ describe("RbacAccessRepository", () => { (1, 42, 'user-1', NULL, 'admin', 'view', NULL, '2026-06-26T00:00:00.000Z'), (2, 42, NULL, 7, 'admin', 'view', '2026-06-27T00:00:00.000Z', '2026-06-26T01:00:00.000Z'), (5, 44, 'user-1', NULL, 'admin', 'view', '2026-06-25T00:00:00.000Z', '2026-06-24T00:00:00.000Z'); - - INSERT INTO shared_host_secrets ( - id, host_access_id, target_user_id, protocol, source_type, original_credential_id, encrypted_username, encrypted_auth_type - ) - VALUES - (8, 2, 'user-1', 'ssh', 'credential', 123, 'enc-user', 'enc-auth'), - (9, 2, 'user-1', 'rdp', 'inline', NULL, 'enc-rdp-user', 'direct'); - - INSERT INTO snippets (id, user_id, name, content) - VALUES (99, 'owner-1', 'deploy', 'echo deploy'); - INSERT INTO snippet_access ( id, snippet_id, user_id, role_id, granted_by, permission_level, expires_at, created_at ) VALUES (3, 99, 'user-1', NULL, 'admin', 'view', NULL, '2026-06-26T00:00:00.000Z'), (4, 99, NULL, 7, 'admin', 'view', '2026-06-27T00:00:00.000Z', '2026-06-26T01:00:00.000Z'); + INSERT INTO shared_host_secrets ( + id, host_access_id, target_user_id, protocol, source_type, original_credential_id, encrypted_username, encrypted_auth_type + ) + VALUES + (8, 2, 'user-1', 'ssh', 'credential', 123, 'enc-user', 'enc-auth'), + (9, 2, 'user-1', 'rdp', 'inline', NULL, 'enc-rdp-user', 'direct'); `); return new RbacAccessRepository(context, onWrite); @@ -436,11 +349,6 @@ describe("RbacAccessRepository", () => { const directAccess = await repo.findDirectHostAccess(42, "user-1"); expect(directAccess?.id).toBe(1); - await repo.updateHostAccessOverrideCredential(1, 123); - expect( - (await repo.findDirectHostAccess(42, "user-1"))?.overrideCredentialId, - ).toBe(123); - await repo.touchHostAccess(1, "2026-06-26T03:00:00.000Z"); expect( (await repo.findDirectHostAccess(42, "user-1"))?.lastAccessedAt, @@ -448,7 +356,7 @@ describe("RbacAccessRepository", () => { await repo.revokeHostAccess(1, 42); expect(await repo.findDirectHostAccess(42, "user-1")).toBeNull(); - expect(writeCount).toBe(5); + expect(writeCount).toBe(4); }); it("finds active host access and deletes expired host access", async () => { diff --git a/src/backend/tests/database/repositories/recent-activity-repository.test.ts b/src/backend/tests/database/repositories/recent-activity-repository.test.ts index 52c0d40..0808794 100644 --- a/src/backend/tests/database/repositories/recent-activity-repository.test.ts +++ b/src/backend/tests/database/repositories/recent-activity-repository.test.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { afterEach, describe, expect, it } from "vitest"; import { TestSqliteDatabase } from "./test-support.js"; import { RecentActivityRepository } from "../../../database/repositories/recent-activity-repository.js"; @@ -16,40 +17,14 @@ describe("RecentActivityRepository", () => { onWrite?: () => void | Promise, ): Promise<{ repository: RecentActivityRepository; - sqlite: NonNullable< - Awaited>["sqlite"] - >; }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE recent_activity ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - type TEXT NOT NULL, - host_id INTEGER NOT NULL, - host_name TEXT, - timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-1', 'two'), (3, 'user-2', 'other'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-1', 'two', '10.0.0.1', 22, 'root', 'password'), (3, 'user-2', 'other', '10.0.0.1', 22, 'root', 'password'); INSERT INTO recent_activity (id, user_id, type, host_id, host_name, timestamp) VALUES (1, 'user-1', 'connect', 1, 'one', '2026-06-26T00:00:00.000Z'), @@ -59,13 +34,12 @@ describe("RecentActivityRepository", () => { return { repository: new RecentActivityRepository(context, onWrite), - sqlite: context.sqlite!, }; } it("lists, creates, and trims recent activity", async () => { let writeCount = 0; - const { repository, sqlite } = await createRepository(() => { + const { repository } = await createRepository(() => { writeCount += 1; }); @@ -88,11 +62,9 @@ describe("RecentActivityRepository", () => { expect(await repository.trimUserActivity("user-1", 2)).toBe(1); expect( - sqlite - .prepare( - "SELECT id FROM recent_activity WHERE user_id = ? ORDER BY timestamp DESC", - ) - .all("user-1"), + await adapter!.query( + sql`SELECT id FROM recent_activity WHERE user_id = 'user-1' ORDER BY timestamp DESC`, + ), ).toEqual([{ id: created.id }, { id: 2 }]); expect(writeCount).toBe(2); }); diff --git a/src/backend/tests/database/repositories/returning.test.ts b/src/backend/tests/database/repositories/returning.test.ts new file mode 100644 index 0000000..42c1111 --- /dev/null +++ b/src/backend/tests/database/repositories/returning.test.ts @@ -0,0 +1,177 @@ +import { sql } from "drizzle-orm"; +import { describe, expect, it } from "vitest"; +import { + deleteReturning, + updateReturning, +} from "../../../database/repositories/returning.js"; +import type { DatabaseContext } from "../../../database/repositories/database-context.js"; +import type { DatabaseDialect } from "../../../database/db/dialect.js"; + +/** + * The MySQL path cannot be exercised against a real engine here, and its whole + * correctness is an ordering property: an update must be read AFTER the write, + * a delete BEFORE it. Get either backwards and the rows describe the wrong + * state โ€” silently, with no error anywhere. + * + * So the drizzle handle is stubbed and the order of calls is recorded. + */ +function recordingContext(dialect: DatabaseDialect) { + const calls: string[] = []; + const rows = [{ id: 1, name: "before" }]; + + const chain = (label: string, result: unknown) => { + calls.push(label); + const thenable = { + set: () => thenable, + from: () => thenable, + where: () => thenable, + returning: () => Promise.resolve(result), + then: (resolve: (v: unknown) => void) => + Promise.resolve(result).then(resolve), + }; + return thenable; + }; + + const db = { + update: () => chain("update", rows), + delete: () => chain("delete", rows), + select: () => chain("select", rows), + transaction: (fn: (tx: unknown) => Promise) => { + calls.push("begin"); + return fn(db).then((value) => { + calls.push("commit"); + return value; + }); + }, + }; + + return { + context: { dialect, drizzle: db } as unknown as DatabaseContext, + calls, + }; +} + +const where = sql`id = 1`; + +describe("updateReturning", () => { + it.each(["sqlite", "postgres"] as const)( + "uses a single statement on %s, where RETURNING exists", + async (dialect) => { + const { context, calls } = recordingContext(dialect); + await updateReturning(context, {} as never, {}, where); + expect(calls).toEqual(["update"]); + }, + ); + + it("on mysql, writes first and reads the new state after", async () => { + const { context, calls } = recordingContext("mysql"); + await updateReturning(context, {} as never, {}, where); + + // Reading first would return the values the update replaced. + expect(calls).toEqual(["begin", "update", "select", "commit"]); + }); +}); + +describe("updateReturning, when the read-back cannot find the rows", () => { + /** + * The failure mode: an update that changes a column its own `where` filters + * on. MySQL writes the rows, then the re-read matches nothing. Returning [] + * would be indistinguishable from "matched nothing" and silently wrong. + */ + function contextThatWritesButCannotReadBack() { + const chain = (result: unknown) => { + const thenable: Record = { + set: () => thenable, + from: () => thenable, + where: () => thenable, + then: (resolve: (v: unknown) => void) => + Promise.resolve(result).then(resolve), + }; + return thenable; + }; + const db = { + update: () => chain({ affectedRows: 3 }), + select: () => chain([]), + transaction: (fn: (tx: unknown) => Promise) => fn(db), + }; + return { dialect: "mysql", drizzle: db } as unknown as DatabaseContext; + } + + it("throws instead of returning an empty array", async () => { + await expect( + updateReturning( + contextThatWritesButCannotReadBack(), + {} as never, + {}, + where, + ), + ).rejects.toThrow(/wrote 3 row\(s\) but could not read them back/); + }); + + it("says how to fix it", async () => { + await expect( + updateReturning( + contextThatWritesButCannotReadBack(), + {} as never, + {}, + where, + ), + ).rejects.toThrow(/filter on a column the update leaves alone/); + }); + + it("still returns [] when the update genuinely matched nothing", async () => { + const { context } = recordingContext("mysql"); + // recordingContext reports rows for select, so use a zero-write stub. + const chain = (result: unknown) => { + const t: Record = { + set: () => t, + from: () => t, + where: () => t, + then: (r: (v: unknown) => void) => Promise.resolve(result).then(r), + }; + return t; + }; + const db = { + update: () => chain({ affectedRows: 0 }), + select: () => chain([]), + transaction: (fn: (tx: unknown) => Promise) => fn(db), + }; + void context; + await expect( + updateReturning( + { dialect: "mysql", drizzle: db } as unknown as DatabaseContext, + {} as never, + {}, + where, + ), + ).resolves.toEqual([]); + }); +}); + +describe("deleteReturning", () => { + it("uses a single statement where RETURNING exists", async () => { + const { context, calls } = recordingContext("postgres"); + await deleteReturning(context, {} as never, where); + expect(calls).toEqual(["delete"]); + }); + + it("on mysql, reads first and deletes after", async () => { + const { context, calls } = recordingContext("mysql"); + const rows = await deleteReturning(context, {} as never, where); + + // Reading after the delete would find nothing at all. + expect(calls).toEqual(["begin", "select", "delete", "commit"]); + expect(rows).toHaveLength(1); + }); + + it("keeps both statements in one transaction", async () => { + const { context, calls } = recordingContext("mysql"); + await deleteReturning(context, {} as never, where); + + // Without this, a concurrent write between them makes the returned rows + // describe a state that never existed โ€” and with a pool the second + // statement need not even reach the same connection. + expect(calls[0]).toBe("begin"); + expect(calls[calls.length - 1]).toBe("commit"); + }); +}); diff --git a/src/backend/tests/database/repositories/role-repository.test.ts b/src/backend/tests/database/repositories/role-repository.test.ts index 585f937..963c595 100644 --- a/src/backend/tests/database/repositories/role-repository.test.ts +++ b/src/backend/tests/database/repositories/role-repository.test.ts @@ -17,45 +17,7 @@ describe("RoleRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - display_name TEXT NOT NULL, - description TEXT, - is_system INTEGER NOT NULL DEFAULT 0, - permissions TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE user_roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - role_id INTEGER NOT NULL, - granted_by TEXT, - granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE host_access ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT, - role_id INTEGER, - granted_by TEXT NOT NULL, - permission_level TEXT NOT NULL DEFAULT 'view', - expires_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash, is_admin, is_oidc) VALUES ('admin', 'admin', 'hash', 1, 0), ('user-1', 'user', 'hash', 0, 0); `); diff --git a/src/backend/tests/database/repositories/session-recording-repository.test.ts b/src/backend/tests/database/repositories/session-recording-repository.test.ts index 7d2437c..9da43e4 100644 --- a/src/backend/tests/database/repositories/session-recording-repository.test.ts +++ b/src/backend/tests/database/repositories/session-recording-repository.test.ts @@ -17,41 +17,11 @@ describe("SessionRecordingRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - ip TEXT - ); - - CREATE TABLE session_recordings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT NOT NULL, - access_id INTEGER, - started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - ended_at TEXT, - duration INTEGER, - commands TEXT, - dangerous_actions TEXT, - recording_path TEXT, - protocol TEXT NOT NULL DEFAULT 'ssh', - format TEXT NOT NULL DEFAULT 'text', - terminated_by_owner INTEGER DEFAULT 0, - termination_reason TEXT - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO ssh_data (id, user_id, name, ip) - VALUES (1, 'user-1', 'one', '10.0.0.1'), (2, 'user-1', 'two', '10.0.0.2'), (3, 'user-2', 'other', '10.0.0.3'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-1', 'two', '10.0.0.2', 22, 'root', 'password'), (3, 'user-2', 'other', '10.0.0.3', 22, 'root', 'password'); `); return new SessionRecordingRepository(context, onWrite); diff --git a/src/backend/tests/database/repositories/session-share-repository.test.ts b/src/backend/tests/database/repositories/session-share-repository.test.ts new file mode 100644 index 0000000..9f8e16c --- /dev/null +++ b/src/backend/tests/database/repositories/session-share-repository.test.ts @@ -0,0 +1,353 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { SessionShareRepository } from "../../../database/repositories/session-share-repository.js"; + +describe("SessionShareRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('owner-1', 'alice', 'hash'), ('guest-1', 'bob', 'hash'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'owner-1', 'host-one', '10.0.0.1', 22, 'root', 'password'), (2, 'owner-1', 'host-two', '10.0.0.2', 22, 'root', 'password'); + `); + + return new SessionShareRepository(context, onWrite); + } + + const FAR_FUTURE = "2999-01-01T00:00:00.000Z"; + const FAR_PAST = "2000-01-01T00:00:00.000Z"; + + it("creates a share and finds it by id", async () => { + const repo = await createRepository(); + + const created = await repo.create({ + id: "share-1", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-abc", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + expect(created).toMatchObject({ + id: "share-1", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + linkToken: "token-abc", + permissionLevel: "read-only", + }); + + const found = await repo.findById("share-1"); + expect(found).toMatchObject({ id: "share-1", sessionId: "session-1" }); + }); + + it("findByLinkToken excludes revoked shares", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-revoked", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-revoked", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + expect(await repo.findByLinkToken("token-revoked")).not.toBeNull(); + + await repo.revoke("share-revoked", "owner-1"); + + expect(await repo.findByLinkToken("token-revoked")).toBeNull(); + }); + + it("findByLinkToken excludes expired shares", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-expired", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-expired", + permissionLevel: "read-only", + expiresAt: FAR_PAST, + }); + + expect(await repo.findByLinkToken("token-expired")).toBeNull(); + }); + + it("findByLinkToken returns active, non-expired, non-revoked shares", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-active", + hostId: 1, + ownerUserId: "owner-1", + protocol: "vnc", + sessionId: "guac-session-1", + shareType: "link", + linkToken: "token-active", + permissionLevel: "read-write", + expiresAt: FAR_FUTURE, + }); + + const found = await repo.findByLinkToken("token-active"); + expect(found).toMatchObject({ + id: "share-active", + protocol: "vnc", + permissionLevel: "read-write", + }); + }); + + it("findSharesTargetingUser returns only active user-targeted shares with host/owner metadata", async () => { + const repo = await createRepository(); + + await repo.create({ + id: "share-user-active", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "user", + targetUserId: "guest-1", + permissionLevel: "read-write", + expiresAt: FAR_FUTURE, + }); + + // Expired user share for the same target - must be excluded + await repo.create({ + id: "share-user-expired", + hostId: 2, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-2", + shareType: "user", + targetUserId: "guest-1", + permissionLevel: "read-only", + expiresAt: FAR_PAST, + }); + + // Link share, not targeting a user - must be excluded even though it's active + await repo.create({ + id: "share-link-active", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-3", + shareType: "link", + linkToken: "token-unrelated", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + const shares = await repo.findSharesTargetingUser("guest-1"); + expect(shares).toHaveLength(1); + expect(shares[0]).toMatchObject({ + id: "share-user-active", + hostName: "host-one", + ownerUsername: "alice", + }); + }); + + it("revoke only affects the requesting owner's own share", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-owned", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-owned", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + expect(await repo.revoke("share-owned", "guest-1")).toBe(false); + expect(await repo.revoke("share-owned", "owner-1")).toBe(true); + }); + + it("revokeAsAdmin revokes regardless of owner", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-admin-target", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-admin", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + expect(await repo.revokeAsAdmin("share-admin-target")).toBe(true); + expect(await repo.findByLinkToken("token-admin")).toBeNull(); + }); + + it("deleteExpiredShares removes only expired rows", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-old", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-old", + permissionLevel: "read-only", + expiresAt: FAR_PAST, + }); + await repo.create({ + id: "share-current", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-2", + shareType: "link", + linkToken: "token-current", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + const deletedCount = await repo.deleteExpiredShares(); + expect(deletedCount).toBe(1); + expect(await repo.findById("share-old")).toBeNull(); + expect(await repo.findById("share-current")).not.toBeNull(); + }); + + it("touchShareUsage increments joinCount and sets lastJoinedAt", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-touch", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-touch", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + await repo.touchShareUsage("share-touch", "2026-01-01T00:00:00.000Z"); + let row = await repo.findById("share-touch"); + expect(row?.joinCount).toBe(1); + expect(row?.lastJoinedAt).toBe("2026-01-01T00:00:00.000Z"); + + await repo.touchShareUsage("share-touch", "2026-01-02T00:00:00.000Z"); + row = await repo.findById("share-touch"); + expect(row?.joinCount).toBe(2); + }); + + it("records and closes participant joins", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-participants", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-participants", + permissionLevel: "read-write", + expiresAt: FAR_FUTURE, + }); + + const participant = await repo.recordParticipantJoin( + "share-participants", + null, + "Guest", + ); + expect(participant).toMatchObject({ + shareId: "share-participants", + userId: null, + guestLabel: "Guest", + }); + expect(participant.leftAt).toBeNull(); + + await repo.recordParticipantLeave(participant.id); + }); + + it("write hook fires on mutating operations", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + await repo.create({ + id: "share-write-hook", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-write-hook", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + expect(writeCount).toBe(1); + + await repo.revoke("share-write-hook", "owner-1"); + expect(writeCount).toBe(2); + }); + + it("deleteSharesForHost removes all shares for a host", async () => { + const repo = await createRepository(); + await repo.create({ + id: "share-host-1a", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "token-h1a", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + await repo.create({ + id: "share-host-1b", + hostId: 1, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-2", + shareType: "link", + linkToken: "token-h1b", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + await repo.create({ + id: "share-host-2", + hostId: 2, + ownerUserId: "owner-1", + protocol: "ssh", + sessionId: "session-3", + shareType: "link", + linkToken: "token-h2", + permissionLevel: "read-only", + expiresAt: FAR_FUTURE, + }); + + expect(await repo.deleteSharesForHost(1)).toBe(2); + expect(await repo.findById("share-host-2")).not.toBeNull(); + }); +}); diff --git a/src/backend/tests/database/repositories/settings-cache-refresh.test.ts b/src/backend/tests/database/repositories/settings-cache-refresh.test.ts new file mode 100644 index 0000000..1f644d4 --- /dev/null +++ b/src/backend/tests/database/repositories/settings-cache-refresh.test.ts @@ -0,0 +1,104 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + refreshIntervalSeconds, + startSettingsCacheRefresh, + stopSettingsCacheRefresh, +} from "../../../database/repositories/factory.js"; + +/** + * The settings cache lives in one process and is updated by whichever process + * wrote the setting. On SQLite that is the only process there is. On Postgres + * and MySQL โ€” the reason those exist here is to let several instances share one + * database โ€” a setting changed on one replica would otherwise never reach the + * others, because the synchronous read cannot go back to the database. + * + * Re-priming on a timer does not make settings immediately consistent. It + * bounds how long they can disagree. + */ +describe("settings cache refresh", () => { + afterEach(() => stopSettingsCacheRefresh()); + + describe("interval", () => { + it("defaults to something short enough to matter", () => { + expect(refreshIntervalSeconds({})).toBe(30); + }); + + it("is configurable", () => { + expect( + refreshIntervalSeconds({ SETTINGS_CACHE_REFRESH_SECONDS: "5" }), + ).toBe(5); + }); + + it("treats zero and nonsense as off", () => { + expect( + refreshIntervalSeconds({ SETTINGS_CACHE_REFRESH_SECONDS: "0" }), + ).toBeNull(); + expect( + refreshIntervalSeconds({ SETTINGS_CACHE_REFRESH_SECONDS: "-1" }), + ).toBeNull(); + expect( + refreshIntervalSeconds({ SETTINGS_CACHE_REFRESH_SECONDS: "soon" }), + ).toBeNull(); + }); + }); + + it("re-reads on the interval", async () => { + const refresh = vi.fn().mockResolvedValue(undefined); + + startSettingsCacheRefresh( + { SETTINGS_CACHE_REFRESH_SECONDS: "0.01" }, + refresh, + ); + + await vi.waitFor(() => + expect(refresh.mock.calls.length).toBeGreaterThan(1), + ); + }); + + it("keeps running after a refresh throws", async () => { + // A transient database blip must not stop the loop, or the replica is stuck + // on stale settings until it restarts โ€” the exact failure this prevents. + const refresh = vi + .fn() + .mockRejectedValueOnce(new Error("connection reset")) + .mockResolvedValue(undefined); + + startSettingsCacheRefresh( + { SETTINGS_CACHE_REFRESH_SECONDS: "0.01" }, + refresh, + ); + + await vi.waitFor(() => + expect(refresh.mock.calls.length).toBeGreaterThan(1), + ); + }); + + it("does nothing when switched off", async () => { + const refresh = vi.fn().mockResolvedValue(undefined); + + startSettingsCacheRefresh({ SETTINGS_CACHE_REFRESH_SECONDS: "0" }, refresh); + await new Promise((resolve) => setTimeout(resolve, 40)); + + expect(refresh).not.toHaveBeenCalled(); + }); + + it("stops when told to, and does not stack timers", async () => { + const refresh = vi.fn().mockResolvedValue(undefined); + + startSettingsCacheRefresh( + { SETTINGS_CACHE_REFRESH_SECONDS: "0.02" }, + refresh, + ); + startSettingsCacheRefresh( + { SETTINGS_CACHE_REFRESH_SECONDS: "0.02" }, + refresh, + ); + + await new Promise((resolve) => setTimeout(resolve, 70)); + stopSettingsCacheRefresh(); + + const afterStop = refresh.mock.calls.length; + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(refresh.mock.calls.length).toBe(afterStop); + }); +}); diff --git a/src/backend/tests/database/repositories/settings-cache.test.ts b/src/backend/tests/database/repositories/settings-cache.test.ts new file mode 100644 index 0000000..81099b3 --- /dev/null +++ b/src/backend/tests/database/repositories/settings-cache.test.ts @@ -0,0 +1,82 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { + forgetCachedSetting, + isSettingsCachePrimed, + primeSettingsCache, + readCachedSetting, + resetSettingsCache, + updateCachedSetting, +} from "../../../database/repositories/settings-cache.js"; + +afterEach(() => resetSettingsCache()); + +describe("settings cache", () => { + it("starts unprimed", () => { + expect(isSettingsCachePrimed()).toBe(false); + }); + + it("reads back what was primed", () => { + primeSettingsCache([ + { key: "guac_url", value: "guacd:4822" }, + { key: "allow_registration", value: "false" }, + ]); + + expect(isSettingsCachePrimed()).toBe(true); + expect(readCachedSetting("guac_url")).toBe("guacd:4822"); + expect(readCachedSetting("allow_registration")).toBe("false"); + }); + + it("returns null for a key that is not set", () => { + primeSettingsCache([{ key: "guac_url", value: "guacd:4822" }]); + + expect(readCachedSetting("missing")).toBeNull(); + }); + + it("returns null rather than throwing before priming", () => { + // Startup ordering means a read can land first. Every caller already + // treats null as "use the default", so this must not throw. + expect(readCachedSetting("guac_url")).toBeNull(); + }); + + it("reflects a write immediately", () => { + primeSettingsCache([{ key: "log_level", value: "info" }]); + + updateCachedSetting("log_level", "debug"); + + // A synchronous reader must not see the pre-write value. + expect(readCachedSetting("log_level")).toBe("debug"); + }); + + it("accepts a key that did not exist at prime time", () => { + primeSettingsCache([]); + + updateCachedSetting("new_key", "value"); + + expect(readCachedSetting("new_key")).toBe("value"); + }); + + it("forgets a deleted key", () => { + primeSettingsCache([{ key: "guac_url", value: "guacd:4822" }]); + + forgetCachedSetting("guac_url"); + + expect(readCachedSetting("guac_url")).toBeNull(); + }); + + it("ignores writes while unprimed instead of half-populating", () => { + // A partially filled cache would be worse than an empty one: readers + // could not tell a real value from a missing prime. + updateCachedSetting("guac_url", "guacd:4822"); + + expect(isSettingsCachePrimed()).toBe(false); + expect(readCachedSetting("guac_url")).toBeNull(); + }); + + it("replaces the previous contents when primed again", () => { + primeSettingsCache([{ key: "old", value: "1" }]); + primeSettingsCache([{ key: "new", value: "2" }]); + + expect(readCachedSetting("old")).toBeNull(); + expect(readCachedSetting("new")).toBe("2"); + }); +}); diff --git a/src/backend/tests/database/repositories/settings-repository.test.ts b/src/backend/tests/database/repositories/settings-repository.test.ts index 94e1ca9..1db7579 100644 --- a/src/backend/tests/database/repositories/settings-repository.test.ts +++ b/src/backend/tests/database/repositories/settings-repository.test.ts @@ -15,12 +15,6 @@ describe("SettingsRepository", () => { async function createRepository(): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ) - `); return new SettingsRepository(context); } diff --git a/src/backend/tests/database/repositories/shared-host-auth-override-repository.test.ts b/src/backend/tests/database/repositories/shared-host-auth-override-repository.test.ts new file mode 100644 index 0000000..3348d6e --- /dev/null +++ b/src/backend/tests/database/repositories/shared-host-auth-override-repository.test.ts @@ -0,0 +1,97 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { sql } from "drizzle-orm"; +import { SharedHostAuthOverrideRepository } from "../../../database/repositories/shared-host-auth-override-repository.js"; +import { TestSqliteDatabase } from "./test-support.js"; + +describe("SharedHostAuthOverrideRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + await adapter?.close(); + adapter = null; + }); + + async function createRepository(onWrite?: () => void) { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('owner', 'owner', 'hash'), ('recipient', 'recipient', 'hash'); + INSERT INTO ssh_data (id, user_id, ip, port, username, auth_type) + VALUES (42, 'owner', '10.0.0.1', 22, 'root', 'password'); + INSERT INTO ssh_credentials (id, user_id, name, auth_type) + VALUES (7, 'recipient', 'cred-seven', 'password'), + (8, 'recipient', 'cred-eight', 'password'); + `); + + return { + repository: new SharedHostAuthOverrideRepository(context, onWrite), + }; + } + + it("creates, reads, updates and clears overrides by host, user, and protocol", async () => { + let writeCount = 0; + const { repository } = await createRepository(() => { + writeCount += 1; + }); + + await expect( + repository.findCredentialId(42, "recipient", "ssh"), + ).resolves.toBeNull(); + + await repository.setCredential(42, "recipient", "ssh", 7); + await expect( + repository.findCredentialId(42, "recipient", "ssh"), + ).resolves.toBe(7); + + await repository.setCredential(42, "recipient", "ssh", 8); + await repository.setCredential(42, "recipient", "rdp", 7); + await expect( + repository.findCredentialId(42, "recipient", "ssh"), + ).resolves.toBe(8); + await expect( + repository.findCredentialId(42, "recipient", "rdp"), + ).resolves.toBe(7); + + await expect( + repository.clearCredential(42, "recipient", "ssh"), + ).resolves.toBe(true); + await expect( + repository.clearCredential(42, "recipient", "ssh"), + ).resolves.toBe(false); + await expect( + repository.findCredentialId(42, "recipient", "rdp"), + ).resolves.toBe(7); + expect(writeCount).toBe(4); + }); + + it("removes overrides when the host, user, or credential is deleted", async () => { + const { repository } = await createRepository(); + + await repository.setCredential(42, "recipient", "ssh", 7); + await adapter!.run(sql`DELETE FROM ssh_credentials WHERE id = 7`); + await expect( + repository.findCredentialId(42, "recipient", "ssh"), + ).resolves.toBeNull(); + + await adapter!.run( + sql`INSERT INTO ssh_credentials (id, user_id, name, auth_type) + VALUES (7, 'recipient', 'cred-seven', 'password')`, + ); + await repository.setCredential(42, "recipient", "ssh", 7); + await adapter!.run(sql`DELETE FROM ssh_data WHERE id = 42`); + await expect( + repository.findCredentialId(42, "recipient", "ssh"), + ).resolves.toBeNull(); + + await adapter!.run( + sql`INSERT INTO ssh_data (id, user_id, ip, port, username, auth_type) + VALUES (42, 'owner', '10.0.0.1', 22, 'root', 'password')`, + ); + await repository.setCredential(42, "recipient", "ssh", 7); + await adapter!.run(sql`DELETE FROM users WHERE id = 'recipient'`); + await expect( + repository.findCredentialId(42, "recipient", "ssh"), + ).resolves.toBeNull(); + }); +}); diff --git a/src/backend/tests/database/repositories/shared-host-secrets-repository.test.ts b/src/backend/tests/database/repositories/shared-host-secrets-repository.test.ts index 75acf71..9eeb568 100644 --- a/src/backend/tests/database/repositories/shared-host-secrets-repository.test.ts +++ b/src/backend/tests/database/repositories/shared-host-secrets-repository.test.ts @@ -16,65 +16,25 @@ describe("SharedHostSecretsRepository", () => { onWrite?: () => void | Promise, ): Promise<{ repository: SharedHostSecretsRepository; - sqlite: NonNullable< - Awaited>["sqlite"] - >; }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite!.exec(` - CREATE TABLE host_access ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT, - role_id INTEGER, - granted_by TEXT NOT NULL, - permission_level TEXT NOT NULL DEFAULT 'connect', - expires_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_accessed_at TEXT, - access_count INTEGER NOT NULL DEFAULT 0, - override_credential_id INTEGER - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT, - ip TEXT NOT NULL, - port INTEGER NOT NULL, - username TEXT NOT NULL, - credential_id INTEGER, - rdp_credential_id INTEGER, - vnc_credential_id INTEGER, - telnet_credential_id INTEGER - ); - - CREATE TABLE shared_host_secrets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_access_id INTEGER NOT NULL, - target_user_id TEXT NOT NULL, - protocol TEXT NOT NULL DEFAULT 'ssh', - source_type TEXT NOT NULL DEFAULT 'credential', - original_credential_id INTEGER, - encrypted_username TEXT, - encrypted_auth_type TEXT, - encrypted_password TEXT, - encrypted_key TEXT, - encrypted_key_password TEXT, - encrypted_key_type TEXT, - encrypted_domain TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(host_access_id, target_user_id, protocol) - ); - - INSERT INTO ssh_data (id, user_id, name, ip, port, username, credential_id, rdp_credential_id) - VALUES - (42, 'owner-1', 'prod', '10.0.0.42', 22, 'root', 123, 124), - (43, 'owner-1', 'staging', '10.0.0.43', 22, 'root', NULL, NULL), - (44, 'owner-2', 'other', '10.0.0.44', 22, 'root', 123, NULL); - + await adapter.exec(` + INSERT INTO users (id, username, password_hash) VALUES + ('owner-1', 'owner-1', 'hash'), + ('owner-2', 'owner-2', 'hash'); + INSERT INTO users (id, username, password_hash) VALUES + ('user-1', 'user-1', 'hash'), + ('user-2', 'user-2', 'hash'); + INSERT INTO roles (id, name, display_name, is_system) VALUES + (7, 'role-7', 'Role 7', 0); + INSERT INTO ssh_credentials (id, user_id, name, username, auth_type) VALUES + (123, 'user-1', 'cred-123', 'root', 'password'), + (124, 'user-1', 'cred-124', 'root', 'password'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, credential_id, rdp_credential_id, auth_type) + VALUES (42, 'owner-1', 'prod', '10.0.0.42', 22, 'root', 123, 124, 'password'), + (43, 'owner-1', 'staging', '10.0.0.43', 22, 'root', NULL, NULL, 'password'), + (44, 'owner-2', 'other', '10.0.0.44', 22, 'root', 123, NULL, 'password'); INSERT INTO host_access (id, host_id, user_id, role_id, granted_by) VALUES (1, 42, 'user-1', NULL, 'owner-1'), @@ -84,7 +44,6 @@ describe("SharedHostSecretsRepository", () => { return { repository: new SharedHostSecretsRepository(context, onWrite), - sqlite: context.sqlite!, }; } diff --git a/src/backend/tests/database/repositories/snippet-repository.test.ts b/src/backend/tests/database/repositories/snippet-repository.test.ts index 8dc1d4c..b7909c5 100644 --- a/src/backend/tests/database/repositories/snippet-repository.test.ts +++ b/src/backend/tests/database/repositories/snippet-repository.test.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { afterEach, describe, expect, it, vi } from "vitest"; import { TestSqliteDatabase } from "./test-support.js"; import { SnippetRepository } from "../../../database/repositories/snippet-repository.js"; @@ -14,35 +15,13 @@ describe("SnippetRepository", () => { async function createRepository(onWrite?: () => void): Promise<{ repository: SnippetRepository; - sqlite: NonNullable< - Awaited>["sqlite"] - >; }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE snippets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - content TEXT NOT NULL, - description TEXT, - folder TEXT, - "order" INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - host_filter TEXT - ); - - CREATE TABLE snippet_folders ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - color TEXT, - icon TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) VALUES + ('user-1', 'user-1', 'hash'), + ('user-2', 'user-2', 'hash'); INSERT INTO snippets ( id, user_id, name, content, description, folder, "order", host_filter @@ -51,7 +30,6 @@ describe("SnippetRepository", () => { (1, 'user-1', 'root', 'uptime', NULL, NULL, 2, NULL), (2, 'user-1', 'deploy', 'make deploy', 'Deploy app', 'ops', 1, 'linux'), (3, 'user-2', 'other', 'whoami', NULL, NULL, 1, NULL); - INSERT INTO snippet_folders (id, user_id, name, color, icon) VALUES (1, 'user-1', 'ops', '#123456', 'terminal'), @@ -61,7 +39,6 @@ describe("SnippetRepository", () => { return { repository: new SnippetRepository(context, onWrite), - sqlite: context.sqlite!, }; } @@ -187,18 +164,18 @@ describe("SnippetRepository", () => { it("deletes all snippets and folders for a user", async () => { const onWrite = vi.fn(); - const { repository, sqlite } = await createRepository(onWrite); + const { repository } = await createRepository(onWrite); await expect(repository.deleteByUserId("user-1")).resolves.toEqual({ snippetsDeleted: 2, foldersDeleted: 2, }); - expect(sqlite.prepare("SELECT id FROM snippets ORDER BY id").all()).toEqual( - [{ id: 3 }], - ); expect( - sqlite.prepare("SELECT id FROM snippet_folders ORDER BY id").all(), + await adapter!.query(sql`SELECT id FROM snippets ORDER BY id`), + ).toEqual([{ id: 3 }]); + expect( + await adapter!.query(sql`SELECT id FROM snippet_folders ORDER BY id`), ).toEqual([{ id: 3 }]); expect(onWrite).toHaveBeenCalledTimes(1); }); diff --git a/src/backend/tests/database/repositories/sql-timestamp.test.ts b/src/backend/tests/database/repositories/sql-timestamp.test.ts new file mode 100644 index 0000000..7707ad7 --- /dev/null +++ b/src/backend/tests/database/repositories/sql-timestamp.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { + formatSqlTimestamp, + sqlTimestampDaysAgo, +} from "../../../database/repositories/sql-timestamp.js"; + +describe("sql timestamps", () => { + it("matches the CURRENT_TIMESTAMP text format", () => { + expect(formatSqlTimestamp(new Date("2026-07-28T01:23:45.678Z"))).toBe( + "2026-07-28 01:23:45", + ); + }); + + it("subtracts whole days in UTC", () => { + const now = new Date("2026-07-28T01:23:45.000Z"); + + expect(sqlTimestampDaysAgo(7, now)).toBe("2026-07-21 01:23:45"); + expect(sqlTimestampDaysAgo(30, now)).toBe("2026-06-28 01:23:45"); + expect(sqlTimestampDaysAgo(0, now)).toBe("2026-07-28 01:23:45"); + }); + + it("crosses month and year boundaries", () => { + expect(sqlTimestampDaysAgo(1, new Date("2026-01-01T00:00:00.000Z"))).toBe( + "2025-12-31 00:00:00", + ); + }); + + it("stays lexicographically ordered, which is what the cutoff comparison relies on", () => { + const now = new Date("2026-07-28T01:23:45.000Z"); + const older = sqlTimestampDaysAgo(30, now); + const newer = sqlTimestampDaysAgo(7, now); + + expect(older < newer).toBe(true); + expect(newer < formatSqlTimestamp(now)).toBe(true); + }); +}); diff --git a/src/backend/tests/database/repositories/ssh-credential-usage-repository.test.ts b/src/backend/tests/database/repositories/ssh-credential-usage-repository.test.ts index 21b25aa..9847b53 100644 --- a/src/backend/tests/database/repositories/ssh-credential-usage-repository.test.ts +++ b/src/backend/tests/database/repositories/ssh-credential-usage-repository.test.ts @@ -17,41 +17,13 @@ describe("SshCredentialUsageRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE ssh_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE ssh_credential_usage ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - credential_id INTEGER NOT NULL, - host_id INTEGER NOT NULL, - user_id TEXT NOT NULL, - used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-1', 'two'), (3, 'user-2', 'other'); - INSERT INTO ssh_credentials (id, user_id, name) - VALUES (1, 'user-1', 'cred-one'), (2, 'user-2', 'cred-two'); + INSERT INTO ssh_credentials (id, user_id, name, username, auth_type) + VALUES (1, 'user-1', 'cred-one', 'root', 'password'), (2, 'user-2', 'cred-two', 'root', 'password'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-1', 'two', '10.0.0.1', 22, 'root', 'password'), (3, 'user-2', 'other', '10.0.0.1', 22, 'root', 'password'); `); return new SshCredentialUsageRepository(context, onWrite); diff --git a/src/backend/tests/database/repositories/sso-provider-repository.test.ts b/src/backend/tests/database/repositories/sso-provider-repository.test.ts index ec0d7cd..7c0b78d 100644 --- a/src/backend/tests/database/repositories/sso-provider-repository.test.ts +++ b/src/backend/tests/database/repositories/sso-provider-repository.test.ts @@ -4,13 +4,11 @@ import { SsoProviderRepository } from "../../../database/repositories/sso-provid describe("SsoProviderRepository", () => { let adapter: TestSqliteDatabase | null = null; - let sqlite: Awaited>["sqlite"]; afterEach(async () => { if (adapter) { await adapter.close(); adapter = null; - sqlite = undefined; } }); @@ -19,28 +17,6 @@ describe("SsoProviderRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - sqlite = context.sqlite; - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0, - sso_provider_id INTEGER - ); - - CREATE TABLE sso_providers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - type TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1, - display_order INTEGER NOT NULL DEFAULT 0, - config TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - `); return new SsoProviderRepository(context, onWrite); } @@ -96,7 +72,7 @@ describe("SsoProviderRepository", () => { config: "{}", }); - sqlite?.exec(` + await adapter!.exec(` INSERT INTO users (id, username, password_hash, sso_provider_id) VALUES ('user-1', 'u1', 'hash', ${provider.id}), ('user-2', 'u2', 'hash', ${provider.id}), diff --git a/src/backend/tests/database/repositories/sync-tombstone-repository.test.ts b/src/backend/tests/database/repositories/sync-tombstone-repository.test.ts new file mode 100644 index 0000000..d03deb2 --- /dev/null +++ b/src/backend/tests/database/repositories/sync-tombstone-repository.test.ts @@ -0,0 +1,109 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { SyncTombstoneRepository } from "../../../database/repositories/sync-tombstone-repository.js"; + +describe("SyncTombstoneRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + `); + + return new SyncTombstoneRepository(context, onWrite); + } + + it("records a tombstone and lists it back for the owning user", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + await repo.record("user-1", "hosts", "sync-abc"); + expect(writeCount).toBe(1); + + const rows = await repo.listSince("user-1", "hosts", null); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + userId: "user-1", + entityType: "hosts", + syncId: "sync-abc", + }); + }); + + it("does not record a tombstone for an empty syncId", async () => { + const repo = await createRepository(); + await repo.record("user-1", "hosts", ""); + const rows = await repo.listSince("user-1", "hosts", null); + expect(rows).toHaveLength(0); + }); + + it("recordMany writes multiple tombstones and filters out falsy ids", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + await repo.recordMany("user-1", "hosts", ["a", "", "b", "c"]); + expect(writeCount).toBe(1); + + const rows = await repo.listSince("user-1", "hosts", null); + expect(rows.map((r) => r.syncId).sort()).toEqual(["a", "b", "c"]); + }); + + it("recordMany is a no-op when given no syncIds", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + await repo.recordMany("user-1", "hosts", []); + expect(writeCount).toBe(0); + }); + + it("scopes listSince by userId and entityType", async () => { + const repo = await createRepository(); + await repo.record("user-1", "hosts", "sync-1"); + await repo.record("user-1", "snippets", "sync-2"); + await repo.record("user-2", "hosts", "sync-3"); + + const rows = await repo.listSince("user-1", "hosts", null); + expect(rows).toHaveLength(1); + expect(rows[0].syncId).toBe("sync-1"); + }); + + it("filters listSince by the since timestamp", async () => { + const adapterLocal = new TestSqliteDatabase(); + adapter = adapterLocal; + const context = await adapterLocal.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'); + INSERT INTO sync_tombstones (user_id, entity_type, sync_id, deleted_at) + VALUES + ('user-1', 'hosts', 'old', '2026-01-01T00:00:00.000Z'), + ('user-1', 'hosts', 'new', '2026-06-01T00:00:00.000Z'); + `); + const repo = new SyncTombstoneRepository(context); + + const rows = await repo.listSince( + "user-1", + "hosts", + "2026-03-01T00:00:00.000Z", + ); + expect(rows).toHaveLength(1); + expect(rows[0].syncId).toBe("new"); + }); +}); diff --git a/src/backend/tests/database/repositories/sync-tombstone-since.test.ts b/src/backend/tests/database/repositories/sync-tombstone-since.test.ts new file mode 100644 index 0000000..89caed1 --- /dev/null +++ b/src/backend/tests/database/repositories/sync-tombstone-since.test.ts @@ -0,0 +1,111 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { SyncTombstoneRepository } from "../../../database/repositories/sync-tombstone-repository.js"; +import { + normalizeSyncTimestamp, + timestampAtOrAfter, +} from "../../../database/sync-timestamp.js"; +import { sshCredentials } from "../../../database/db/schema.js"; +import type { DatabaseContext } from "../../../database/repositories/database-context.js"; +import { and, eq } from "drizzle-orm"; + +// The desktop sync engine always sends its cursor as new Date().toISOString(). +const ISO_CURSOR = "2026-07-29T09:00:00.000Z"; + +describe("sync cursors across timestamp layouts", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + /** + * The harness migrates the schema itself, so the seeds below are INSERTs into + * the real tables. Both `sync_tombstones.user_id` and `ssh_credentials.user_id` + * are foreign keys into `users`, which the harness enforces, so the owning row + * has to exist before either seed runs. + */ + async function connect(): Promise<{ + db: TestSqliteDatabase; + context: DatabaseContext; + }> { + const db = new TestSqliteDatabase(); + adapter = db; + const context = await db.connect(); + await db.exec(` + INSERT INTO users (id, username, password_hash) VALUES + ('user-1', 'user', 'hash'); + `); + return { db, context }; + } + + it("normalizes both layouts to one comparable form", () => { + expect(normalizeSyncTimestamp("2026-07-29T10:11:21.123Z")).toBe( + "2026-07-29 10:11:21", + ); + expect(normalizeSyncTimestamp("2026-07-29 10:11:21")).toBe( + "2026-07-29 10:11:21", + ); + }); + + it("returns tombstones recorded after an ISO cursor, whatever layout they were stored in", async () => { + const { db, context } = await connect(); + await db.exec(` + INSERT INTO sync_tombstones (user_id, entity_type, sync_id, deleted_at) VALUES + ('user-1', 'sshCredentials', 'sqlite-layout', '2026-07-29 10:07:32'), + ('user-1', 'sshCredentials', 'iso-layout', '2026-07-29T10:07:32.500Z'), + ('user-1', 'sshCredentials', 'too-old', '2026-07-29 08:00:00'); + `); + + const repo = new SyncTombstoneRepository(context); + const rows = await repo.listSince("user-1", "sshCredentials", ISO_CURSOR); + + expect(rows.map((row) => row.syncId).sort()).toEqual([ + "iso-layout", + "sqlite-layout", + ]); + }); + + it("returns rows written by CURRENT_TIMESTAMP against an ISO cursor", async () => { + const { db, context } = await connect(); + await db.exec(` + INSERT INTO ssh_credentials (user_id, name, auth_type, updated_at) VALUES + ('user-1', 'newer-sqlite-layout', 'password', '2026-07-29 10:11:21'), + ('user-1', 'newer-iso-layout', 'password', '2026-07-29T10:11:21.123Z'), + ('user-1', 'older', 'password', '2026-07-29 08:59:59'); + `); + + const rows = await context.drizzle + .select({ name: sshCredentials.name }) + .from(sshCredentials) + .where( + and( + eq(sshCredentials.userId, "user-1"), + timestampAtOrAfter(sshCredentials.updatedAt, ISO_CURSOR), + ), + ); + + expect(rows.map((row) => row.name).sort()).toEqual([ + "newer-iso-layout", + "newer-sqlite-layout", + ]); + }); + + it("keeps rows written in the same second as the cursor", async () => { + const { db, context } = await connect(); + await db.exec(` + INSERT INTO ssh_credentials (user_id, name, auth_type, updated_at) + VALUES ('user-1', 'same-second', 'password', '2026-07-29 09:00:00'); + `); + + const rows = await context.drizzle + .select({ name: sshCredentials.name }) + .from(sshCredentials) + .where(timestampAtOrAfter(sshCredentials.updatedAt, ISO_CURSOR)); + + expect(rows.map((row) => row.name)).toEqual(["same-second"]); + }); +}); diff --git a/src/backend/tests/database/repositories/termix-identity-ca-repository.test.ts b/src/backend/tests/database/repositories/termix-identity-ca-repository.test.ts index 26d2119..ce099d5 100644 --- a/src/backend/tests/database/repositories/termix-identity-ca-repository.test.ts +++ b/src/backend/tests/database/repositories/termix-identity-ca-repository.test.ts @@ -1,4 +1,5 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { sql } from "drizzle-orm"; +import { afterEach, describe, expect, vi } from "vitest"; import { TestSqliteDatabase } from "./test-support.js"; import { DataCrypto } from "../../../utils/data-crypto.js"; import { TermixIdentityCaRepository } from "../../../database/repositories/termix-identity-ca-repository.js"; @@ -16,45 +17,11 @@ describe("TermixIdentityCaRepository", () => { async function createRepository(onWrite = vi.fn()): Promise<{ repo: TermixIdentityCaRepository; - sqlite: NonNullable< - Awaited>["sqlite"] - >; onWrite: ReturnType; }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE termix_identities ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL UNIQUE, - handle TEXT NOT NULL UNIQUE, - description TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - - CREATE TABLE termix_identity_ca ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - identity_id INTEGER NOT NULL UNIQUE, - user_id TEXT NOT NULL, - public_key TEXT NOT NULL, - private_key TEXT NOT NULL, - validity_days INTEGER NOT NULL DEFAULT 90, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (identity_id) REFERENCES termix_identities(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'); INSERT INTO termix_identities (id, user_id, handle) @@ -63,7 +30,6 @@ describe("TermixIdentityCaRepository", () => { return { repo: new TermixIdentityCaRepository(context, onWrite), - sqlite: context.sqlite!, onWrite, }; } @@ -95,7 +61,7 @@ describe("TermixIdentityCaRepository", () => { } it("creates CA private keys with the real row id before encryption", async () => { - const { repo, sqlite, onWrite } = await createRepository(); + const { repo, onWrite } = await createRepository(); mockCrypto(); const created = await repo.createEncryptedForUser("user-1", { @@ -106,16 +72,14 @@ describe("TermixIdentityCaRepository", () => { validityDays: 120, }); - const raw = sqlite - .prepare( - "SELECT id, public_key, private_key, validity_days FROM termix_identity_ca WHERE identity_id = ?", - ) - .get(7) as { + const [raw] = (await adapter!.query( + sql`SELECT id, public_key, private_key, validity_days FROM termix_identity_ca WHERE identity_id = 7`, + )) as { id: number; public_key: string; private_key: string; validity_days: number; - }; + }[]; expect(created.privateKey).toBe("decrypted-ca-private"); expect(raw.private_key).toBe("encrypted-ca-private"); @@ -131,13 +95,12 @@ describe("TermixIdentityCaRepository", () => { }); it("reads public CA metadata without decrypting private key material", async () => { - const { repo, sqlite } = await createRepository(); + const { repo } = await createRepository(); const decryptSpy = vi.spyOn(DataCrypto, "decryptRecord"); - sqlite - .prepare( - "INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (?, ?, ?, ?, ?)", - ) - .run(7, "user-1", "ssh-ed25519 public", "encrypted-ca-private", 45); + await adapter!.run( + sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) + VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`, + ); await expect(repo.findPublicByIdentityId(7)).resolves.toEqual({ publicKey: "ssh-ed25519 public", @@ -147,13 +110,12 @@ describe("TermixIdentityCaRepository", () => { }); it("decrypts CA private keys through the user data boundary", async () => { - const { repo, sqlite } = await createRepository(); + const { repo } = await createRepository(); mockCrypto(); - sqlite - .prepare( - "INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (?, ?, ?, ?, ?)", - ) - .run(7, "user-1", "ssh-ed25519 public", "encrypted-ca-private", 45); + await adapter!.run( + sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) + VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`, + ); const ca = await repo.findDecryptedByIdentityId("user-1", 7); @@ -172,13 +134,11 @@ describe("TermixIdentityCaRepository", () => { }); it("updates CA private keys through encrypted writes", async () => { - const { repo, sqlite, onWrite } = await createRepository(); + const { repo, onWrite } = await createRepository(); mockCrypto(); - sqlite - .prepare( - "INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (?, ?, ?, ?, ?)", - ) - .run(7, "user-1", "ssh-ed25519 old", "encrypted-ca-private", 45); + await adapter!.run( + sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (7, 'user-1', 'ssh-ed25519 old', 'encrypted-ca-private', 45)`, + ); onWrite.mockClear(); const updated = await repo.updateEncryptedForIdentity("user-1", 7, { @@ -187,15 +147,13 @@ describe("TermixIdentityCaRepository", () => { validityDays: 90, }); - const raw = sqlite - .prepare( - "SELECT public_key, private_key, validity_days FROM termix_identity_ca WHERE identity_id = ?", - ) - .get(7) as { + const [raw] = (await adapter!.query( + sql`SELECT public_key, private_key, validity_days FROM termix_identity_ca WHERE identity_id = 7`, + )) as { public_key: string; private_key: string; validity_days: number; - }; + }[]; expect(updated).toMatchObject({ publicKey: "ssh-ed25519 new", @@ -219,55 +177,47 @@ describe("TermixIdentityCaRepository", () => { }); it("deletes CA rows through the write boundary", async () => { - const { repo, sqlite, onWrite } = await createRepository(); - sqlite - .prepare( - "INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (?, ?, ?, ?, ?)", - ) - .run(7, "user-1", "ssh-ed25519 public", "encrypted-ca-private", 45); + const { repo, onWrite } = await createRepository(); + await adapter!.run( + sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`, + ); onWrite.mockClear(); await expect(repo.deleteByIdentityId(7)).resolves.toBe(true); await expect(repo.deleteByIdentityId(7)).resolves.toBe(false); expect( - sqlite.prepare("SELECT COUNT(*) AS count FROM termix_identity_ca").get(), - ).toEqual({ count: 0 }); + ( + await adapter!.query( + sql`SELECT COUNT(*) AS count FROM termix_identity_ca`, + ) + ).map((row) => Number((row as { count: unknown }).count)), + ).toEqual([0]); expect(onWrite).toHaveBeenCalledTimes(1); }); it("deletes CA rows for a user", async () => { - const { repo, sqlite, onWrite } = await createRepository(); - sqlite - .prepare( - "INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)", - ) - .run("user-2", "bob", "hash"); - sqlite - .prepare( - "INSERT INTO termix_identities (id, user_id, handle) VALUES (?, ?, ?)", - ) - .run(8, "user-2", "bob"); - sqlite - .prepare( - "INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (?, ?, ?, ?, ?)", - ) - .run(7, "user-1", "ssh-ed25519 public", "encrypted-ca-private", 45); - sqlite - .prepare( - "INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (?, ?, ?, ?, ?)", - ) - .run(8, "user-2", "ssh-ed25519 other", "encrypted-other", 90); + const { repo, onWrite } = await createRepository(); + await adapter!.run( + sql`INSERT INTO users (id, username, password_hash) VALUES ('user-2', 'bob', 'hash')`, + ); + await adapter!.run( + sql`INSERT INTO termix_identities (id, user_id, handle) VALUES (8, 'user-2', 'bob')`, + ); + await adapter!.run( + sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`, + ); + await adapter!.run( + sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (8, 'user-2', 'ssh-ed25519 other', 'encrypted-other', 90)`, + ); onWrite.mockClear(); await expect(repo.deleteByUserId("user-1")).resolves.toBe(1); await expect(repo.deleteByUserId("missing")).resolves.toBe(0); expect( - sqlite - .prepare( - "SELECT user_id, public_key FROM termix_identity_ca ORDER BY user_id", - ) - .all(), + await adapter!.query( + sql`SELECT user_id, public_key FROM termix_identity_ca ORDER BY user_id`, + ), ).toEqual([{ user_id: "user-2", public_key: "ssh-ed25519 other" }]); expect(onWrite).toHaveBeenCalledTimes(1); }); diff --git a/src/backend/tests/database/repositories/termix-identity-repository.test.ts b/src/backend/tests/database/repositories/termix-identity-repository.test.ts index b9c2ad2..c801faf 100644 --- a/src/backend/tests/database/repositories/termix-identity-repository.test.ts +++ b/src/backend/tests/database/repositories/termix-identity-repository.test.ts @@ -18,44 +18,12 @@ describe("TermixIdentityRepository", () => { }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE termix_identities ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL UNIQUE, - handle TEXT NOT NULL UNIQUE, - description TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - - CREATE TABLE termix_identity_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - identity_id INTEGER NOT NULL, - user_id TEXT NOT NULL, - public_key TEXT NOT NULL, - key_type TEXT NOT NULL, - algorithm TEXT NOT NULL, - label TEXT, - comment TEXT, - source TEXT NOT NULL DEFAULT 'manual', - credential_id INTEGER, - enabled INTEGER NOT NULL DEFAULT 1, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (identity_id) REFERENCES termix_identities(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO ssh_credentials (id, user_id, name, username, auth_type) VALUES + (10, 'user-1', 'cred-10', 'root', 'password'), + (20, 'user-1', 'cred-20', 'root', 'password'); `); return { diff --git a/src/backend/tests/database/repositories/test-support.ts b/src/backend/tests/database/repositories/test-support.ts index 6aab29f..b92d673 100644 --- a/src/backend/tests/database/repositories/test-support.ts +++ b/src/backend/tests/database/repositories/test-support.ts @@ -1,31 +1,471 @@ import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; +import { + getTableColumns, + getTableName, + is, + sql, + Table, + type SQL, +} from "drizzle-orm"; +import fs from "fs"; +import path from "path"; import * as schema from "../../../database/db/schema.js"; import type { DatabaseContext } from "../../../database/repositories/database-context.js"; +import type { DatabaseDialect } from "../../../database/db/dialect.js"; + +/** + * Which engine the repository tests run against. + * + * Defaults to SQLite, so `npm test` behaves as it always has and needs no + * server. Set TEST_DIALECT=postgres or mysql, plus TEST_DATABASE_URL, to run + * the same tests against a real one โ€” see the database-dialects CI job. + */ +export function testDialect(env = process.env): DatabaseDialect { + const value = env.TEST_DIALECT?.trim().toLowerCase(); + if (value === "postgres" || value === "mysql") return value; + return "sqlite"; +} + +/** Every table drizzle knows about, for wiping between tests. */ +function allTableNames(): string[] { + return Object.values(schema) + .filter((value) => is(value, Table)) + .map((table) => getTableName(table as Table)); +} export class TestSqliteDatabase { private sqlite: Database.Database | null = null; private context: DatabaseContext | null = null; + private readonly dialect: DatabaseDialect; + + constructor(dialect: DatabaseDialect = testDialect()) { + this.dialect = dialect; + } async connect(): Promise { if (this.context) return this.context; + if (this.dialect !== "sqlite") { + this.context = await this.connectRemote(); + return this.context; + } + this.sqlite = new Database(":memory:"); this.sqlite.exec("PRAGMA foreign_keys = ON"); + this.sqlite.exec(sqliteSchemaSql()); this.context = { dialect: "sqlite", drizzle: drizzle(this.sqlite, { schema }), - sqlite: this.sqlite, }; return this.context; } + private async connectRemote(): Promise { + const url = process.env.TEST_DATABASE_URL; + if (!url) { + throw new Error( + `TEST_DIALECT=${this.dialect} requires TEST_DATABASE_URL to be set.`, + ); + } + + const { drizzle: connect } = await import( + this.dialect === "postgres" + ? "drizzle-orm/node-postgres" + : "drizzle-orm/mysql2" + ); + const db = connect(url) as unknown as DatabaseContext["drizzle"]; + const context: DatabaseContext = { dialect: this.dialect, drizzle: db }; + + await migrateOnce(this.dialect, db); + await truncateAll(context); + + return context; + } + + /** + * Runs seed SQL. Synchronous on SQLite, which is what the tests were written + * against; on the other engines it returns a promise the caller must await. + * + * The seeds are plain INSERTs, portable apart from identifier quoting, which + * `portableSql` fixes up. + */ + exec(statements: string): void | Promise { + if (this.sqlite) { + this.sqlite.exec(statements); + return; + } + const context = this.context; + if (!context) throw new Error("connect() must be called before exec()"); + + return (async () => { + const touched = new Set(); + for (const statement of splitStatements(statements)) { + await runSql(context, sql.raw(portableSql(statement, context.dialect))); + const table = /INSERT INTO\s+([a-z_]+)/i.exec(statement)?.[1]; + if (table) touched.add(table); + } + await resyncAutoIncrement(context, touched); + })(); + } + + /** + * Portable read for assertions. Build the statement with drizzle's `sql` + * template so placeholders and quoting come out right on each engine. + */ + async query>(statement: SQL): Promise { + if (!this.context) + throw new Error("connect() must be called before query()"); + return runSql(this.context, statement); + } + + /** + * Portable write for test setup. + * + * Separate from query() because better-sqlite3 refuses `.all()` on a + * statement that returns no rows โ€” "This statement does not return data". + */ + async run(statement: SQL): Promise { + const context = this.context; + if (!context) throw new Error("connect() must be called before run()"); + + if (this.sqlite) { + (context.drizzle as unknown as { run: (s: SQL) => unknown }).run( + statement, + ); + return; + } + await runSql(context, statement); + } + async close(): Promise { if (this.sqlite) { this.sqlite.close(); this.sqlite = null; - this.context = null; + } + this.context = null; + } +} + +async function runSql( + context: DatabaseContext, + statement: SQL, +): Promise { + const db = context.drizzle as unknown as { + all?: (s: SQL) => Promise | T[]; + execute?: (s: SQL) => Promise; + }; + + if (context.dialect === "sqlite" && db.all) { + return (await db.all(statement)) as T[]; + } + + const result = (await db.execute!(statement)) as + { rows?: T[] } | T[] | undefined; + + // mysql2 answers [rows, fields]; node-postgres answers { rows }. + if (Array.isArray(result)) { + return (Array.isArray(result[0]) ? result[0] : result) as T[]; + } + return (result?.rows ?? []) as T[]; +} + +/** + * Empties every table between tests on the client-server engines, where the + * database outlives the process and cannot be thrown away like an in-memory + * SQLite one. + */ +async function truncateAll(context: DatabaseContext): Promise { + const tables = allTableNames(); + + if (context.dialect === "postgres") { + const list = tables.map((t) => `"${t}"`).join(", "); + await runSql( + context, + sql.raw(`TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE`), + ); + return; + } + + // Truncating all 53 tables takes ~2s on MySQL, which every test would pay. + // Ask which ones actually hold rows first: after the first test only a + // handful do, and the check is a single query. + // Each branch is parenthesised: LIMIT binds to the whole UNION otherwise. + const counts = tables + .map((t) => `(SELECT '${t}' AS name FROM \`${t}\` LIMIT 1)`) + .join(" UNION ALL "); + const occupied = await runSql<{ name: string }>(context, sql.raw(counts)); + if (occupied.length === 0) return; + + await runSql(context, sql.raw("SET FOREIGN_KEY_CHECKS = 0")); + for (const { name } of occupied) { + await runSql(context, sql.raw(`TRUNCATE TABLE \`${name}\``)); + } + await runSql(context, sql.raw("SET FOREIGN_KEY_CHECKS = 1")); +} + +/** + * Splits seed SQL into statements, ignoring semicolons inside string literals โ€” + * JSON payloads in the fixtures contain them. + */ +function splitStatements(sql: string): string[] { + const out: string[] = []; + let current = ""; + let inString = false; + + for (let i = 0; i < sql.length; i++) { + const ch = sql[i]; + if (ch === "'") { + // '' is an escaped quote inside a string, not a delimiter. + if (inString && sql[i + 1] === "'") { + current += "''"; + i++; + continue; + } + inString = !inString; + } + if (ch === ";" && !inString) { + if (current.trim()) out.push(current.trim()); + current = ""; + continue; + } + current += ch; + } + if (current.trim()) out.push(current.trim()); + return out; +} + +let cachedBooleanColumns: Set | null = null; + +/** + * Columns the schema declares as booleans, by table.column. + * + * Read from drizzle rather than listed here, so a new boolean column needs no + * change in this file. + */ +function booleanColumns(): Set { + if (cachedBooleanColumns) return cachedBooleanColumns; + + const found = new Set(); + for (const value of Object.values(schema)) { + if (!is(value, Table)) continue; + const table = getTableName(value as Table); + for (const column of Object.values(getTableColumns(value as Table))) { + if (column.dataType === "boolean") found.add(`${table}.${column.name}`); + } + } + cachedBooleanColumns = found; + return found; +} + +/** + * Seeds are written in SQLite's dialect. Two things do not carry: + * + * - a reserved word used as a column name is `"order"` on SQLite and Postgres, + * `` `order` `` on MySQL + * - SQLite stores booleans as 0/1, and writing an integer into a native boolean + * column is an error on Postgres. Every engine understands the TRUE/FALSE + * keywords, so boolean columns are rewritten to those. + */ +function portableSql(statement: string, dialect: DatabaseDialect): string { + const out = rewriteBooleanLiterals(statement); + if (dialect !== "mysql") return out; + + // Only the column list, before VALUES. A blanket replace also mangles the + // double quotes inside JSON payloads in the values โ€” '{"slots":[]}' became + // '{`slots`:[]}', which is valid SQL and silently wrong data. + const split = /^(.*?\bVALUES\b)(.*)$/is.exec(out); + if (!split) return out.replace(/"([a-z_]+)"/g, "`$1`"); + return split[1].replace(/"([a-z_]+)"/g, "`$1`") + split[2]; +} + +/** Rewrites 0/1 to FALSE/TRUE in the value positions of boolean columns. */ +function rewriteBooleanLiterals(statement: string): string { + const booleans = booleanColumns(); + + return statement.replace( + /INSERT INTO\s+([a-z_]+)\s*\(([^)]*)\)\s*VALUES\s*((?:\([^()]*\)\s*,?\s*)+)/gis, + (whole, table: string, cols: string, values: string) => { + const names = cols.split(",").map((c) => c.trim().replace(/["`]/g, "")); + const flags = names.map((n) => booleans.has(`${table}.${n}`)); + if (!flags.some(Boolean)) return whole; + + const rewritten = values.replace( + /\(([^()]*)\)/g, + (row, inner: string) => { + const parts = splitValues(inner); + return `(${parts + .map((v, i) => + flags[i] && /^[01]$/.test(v.trim()) + ? v.trim() === "1" + ? "TRUE" + : "FALSE" + : v, + ) + .join(",")})`; + }, + ); + return `INSERT INTO ${table} (${cols}) VALUES ${rewritten}`; + }, + ); +} + +/** Splits a VALUES row on commas that are not inside a string literal. */ +function splitValues(row: string): string[] { + const parts: string[] = []; + let current = ""; + let inString = false; + for (let i = 0; i < row.length; i++) { + const ch = row[i]; + if (ch === "'") { + if (inString && row[i + 1] === "'") { + current += "''"; + i++; + continue; + } + inString = !inString; + } + if (ch === "," && !inString) { + parts.push(current); + current = ""; + continue; + } + current += ch; + } + parts.push(current); + return parts; +} + +/** + * Moves each table's id generator past the ids the seed inserted by hand. + * + * SQLite picks `max(id) + 1` when a row omits the key, so a fixture that writes + * `id = 1, 2, 3` and then lets the repository insert one more just works. A + * Postgres sequence or a MySQL auto_increment counter does not know about rows + * inserted with an explicit id, so it hands out 1 again and the insert collides + * with the fixture's own data. + */ +async function resyncAutoIncrement( + context: DatabaseContext, + tables: Set, +): Promise { + for (const table of tables) { + // Only tables whose id is generated. A text primary key, like users.id, + // has no sequence and no counter to move, and a table keyed on something + // else entirely โ€” host_sidebar_preferences.user_id โ€” has no id at all. + if (context.dialect === "postgres") { + // pg_get_serial_sequence() raises 42703 rather than returning null when + // the column is missing, so let information_schema decide whether there + // is an id to ask about: no id column yields no row. + const [seq] = await runSql<{ name: string | null }>( + context, + sql.raw( + `SELECT pg_get_serial_sequence('${table}', 'id') AS name ` + + `FROM information_schema.columns ` + + `WHERE table_schema = current_schema() AND table_name = '${table}' ` + + `AND column_name = 'id'`, + ), + ); + if (!seq?.name) continue; + + await runSql( + context, + sql.raw( + `SELECT setval('${seq.name}', ` + + `COALESCE((SELECT MAX(id) FROM "${table}"), 0) + 1, false)`, + ), + ); + continue; + } + + const [column] = await runSql<{ extra: string }>( + context, + sql.raw( + `SELECT EXTRA AS extra FROM information_schema.columns ` + + `WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${table}' ` + + `AND COLUMN_NAME = 'id'`, + ), + ); + if (!column?.extra?.includes("auto_increment")) continue; + + const [row] = await runSql<{ next: number | null }>( + context, + sql.raw(`SELECT MAX(id) + 1 AS next FROM \`${table}\``), + ); + if (row?.next) { + await runSql( + context, + sql.raw(`ALTER TABLE \`${table}\` AUTO_INCREMENT = ${row.next}`), + ); } } } + +/** + * Migrations run once per worker, not once per fixture. + * + * Every test builds a fixture, and each would otherwise re-run the migrator + * against the same shared database. drizzle's journal makes that a no-op only + * when the first run finished โ€” several fixtures racing inside one file hit + * "table already exists" instead. + */ +const migrations = new Map>(); + +function migrateOnce( + dialect: DatabaseDialect, + db: DatabaseContext["drizzle"], +): Promise { + const key = `${dialect}:${process.env.TEST_DATABASE_URL}`; + let running = migrations.get(key); + if (!running) { + running = (async () => { + const { runRemoteMigrations } = + await import("../../../database/db/migrate.js"); + await runRemoteMigrations(dialect, db); + })(); + migrations.set(key, running); + } + return running; +} + +let cachedSqliteSchema: string | null = null; + +/** + * The full schema, from the generated SQLite migrations rather than + * hand-written DDL in each test file. + * + * Tests used to declare a cut-down version of every table they touched โ€” a + * `users` with five columns where the real one has thirty. That drifts from the + * schema silently, and it is the reason the same tests could not be pointed at + * another engine. + * + * There can be more than one migration file (a baseline plus later + * incremental ones): drizzle-kit numbers them `0000_`, `0001_`, ... in + * generation order, so replaying every file in that (lexical) order + * reconstructs the current schema exactly like a real migration run would. + */ +function sqliteSchemaSql(): string { + if (cachedSqliteSchema) return cachedSqliteSchema; + + const dir = path.resolve(process.cwd(), "drizzle", "sqlite"); + const files = fs + .readdirSync(dir) + .filter((name) => name.endsWith(".sql")) + .sort(); + + if (files.length === 0) { + throw new Error(`No SQLite migration found in ${dir}`); + } + + cachedSqliteSchema = files + .map((file) => + fs + .readFileSync(path.join(dir, file), "utf8") + .split("--> statement-breakpoint") + .join("\n"), + ) + .join("\n"); + + return cachedSqliteSchema; +} diff --git a/src/backend/tests/database/repositories/tmux-session-tag-repository.test.ts b/src/backend/tests/database/repositories/tmux-session-tag-repository.test.ts index a866b54..f55be5b 100644 --- a/src/backend/tests/database/repositories/tmux-session-tag-repository.test.ts +++ b/src/backend/tests/database/repositories/tmux-session-tag-repository.test.ts @@ -17,32 +17,11 @@ describe("TmuxSessionTagRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE tmux_session_tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - session_name TEXT NOT NULL, - tag TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-2', 'two'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '10.0.0.1', 22, 'root', 'password'); INSERT INTO tmux_session_tags (user_id, host_id, session_name, tag) VALUES ('user-1', 1, 'api', 'prod'), diff --git a/src/backend/tests/database/repositories/transfer-recent-repository.test.ts b/src/backend/tests/database/repositories/transfer-recent-repository.test.ts index 5ad9783..65d2ecc 100644 --- a/src/backend/tests/database/repositories/transfer-recent-repository.test.ts +++ b/src/backend/tests/database/repositories/transfer-recent-repository.test.ts @@ -17,33 +17,11 @@ describe("TransferRecentRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE transfer_recent ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - source_host_id INTEGER NOT NULL, - dest_host_id INTEGER NOT NULL, - dest_path TEXT NOT NULL, - dest_path_label TEXT NOT NULL, - last_used TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO hosts (id, user_id, name) - VALUES (1, 'user-1', 'source'), (2, 'user-1', 'dest-a'), (3, 'user-1', 'dest-b'), (4, 'user-2', 'other'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES (1, 'user-1', 'source', '10.0.0.1', 22, 'root', 'password'), (2, 'user-1', 'dest-a', '10.0.0.1', 22, 'root', 'password'), (3, 'user-1', 'dest-b', '10.0.0.1', 22, 'root', 'password'), (4, 'user-2', 'other', '10.0.0.1', 22, 'root', 'password'); `); return new TransferRecentRepository(context, onWrite); diff --git a/src/backend/tests/database/repositories/trusted-device-repository.test.ts b/src/backend/tests/database/repositories/trusted-device-repository.test.ts index 290bc10..80e7c06 100644 --- a/src/backend/tests/database/repositories/trusted-device-repository.test.ts +++ b/src/backend/tests/database/repositories/trusted-device-repository.test.ts @@ -17,27 +17,7 @@ describe("TrustedDeviceRepository", () => { }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE trusted_devices ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - device_fingerprint TEXT NOT NULL, - device_type TEXT NOT NULL, - device_info TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'admin', 'hash'), ('user-2', 'user', 'hash'); diff --git a/src/backend/tests/database/repositories/ui-preference-repository.test.ts b/src/backend/tests/database/repositories/ui-preference-repository.test.ts new file mode 100644 index 0000000..504f593 --- /dev/null +++ b/src/backend/tests/database/repositories/ui-preference-repository.test.ts @@ -0,0 +1,92 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { UiPreferenceRepository } from "../../../database/repositories/ui-preference-repository.js"; + +describe("UiPreferenceRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + INSERT INTO ui_preferences (user_id, data, updated_at) + VALUES ( + 'user-1', + '{"version":1,"preset":"balanced","overrides":{}}', + '2026-01-01T00:00:00.000Z' + ); + `); + return new UiPreferenceRepository(context, onWrite); + } + + it("finds saved UI preferences by user id", async () => { + const repository = await createRepository(); + + const existing = await repository.findByUserId("user-1"); + expect(existing?.data).toBe( + '{"version":1,"preset":"balanced","overrides":{}}', + ); + + expect(await repository.findByUserId("user-2")).toBeNull(); + }); + + it("updates and inserts preferences with write notifications", async () => { + let writeCount = 0; + const repository = await createRepository(() => { + writeCount += 1; + }); + + const updated = await repository.upsert( + "user-1", + '{"version":1,"preset":"simple","overrides":{}}', + "2026-02-01T00:00:00.000Z", + ); + expect(updated).toMatchObject({ + userId: "user-1", + data: '{"version":1,"preset":"simple","overrides":{}}', + updatedAt: "2026-02-01T00:00:00.000Z", + }); + + const created = await repository.upsert( + "user-2", + '{"version":1,"preset":"advanced","overrides":{}}', + "2026-03-01T00:00:00.000Z", + ); + expect(created).toMatchObject({ + userId: "user-2", + data: '{"version":1,"preset":"advanced","overrides":{}}', + updatedAt: "2026-03-01T00:00:00.000Z", + }); + + expect(writeCount).toBe(2); + }); + + it("deletes preferences for a user", async () => { + let writeCount = 0; + const repository = await createRepository(() => { + writeCount += 1; + }); + + await repository.upsert("user-2", '{"version":1,"preset":"simple"}'); + + await expect(repository.deleteByUserId("user-1")).resolves.toBe(1); + await expect(repository.deleteByUserId("missing")).resolves.toBe(0); + + expect(await repository.findByUserId("user-1")).toBeNull(); + expect((await repository.findByUserId("user-2"))?.data).toBe( + '{"version":1,"preset":"simple"}', + ); + expect(writeCount).toBe(2); + }); +}); diff --git a/src/backend/tests/database/repositories/user-data-export-repository.test.ts b/src/backend/tests/database/repositories/user-data-export-repository.test.ts index d52c2c3..e218522 100644 --- a/src/backend/tests/database/repositories/user-data-export-repository.test.ts +++ b/src/backend/tests/database/repositories/user-data-export-repository.test.ts @@ -15,142 +15,17 @@ describe("UserDataExportRepository", () => { async function createRepository(): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE ssh_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - connection_type TEXT NOT NULL DEFAULT 'ssh', - name TEXT, - ip TEXT NOT NULL, - port INTEGER NOT NULL, - username TEXT NOT NULL, - folder TEXT, - tags TEXT, - pin INTEGER NOT NULL DEFAULT 0, - auth_type TEXT NOT NULL, - use_warpgate INTEGER NOT NULL DEFAULT 0, - force_keyboard_interactive TEXT, - password TEXT, - key TEXT, - key_password TEXT, - key_type TEXT, - sudo_password TEXT, - autostart_password TEXT, - autostart_key TEXT, - autostart_key_password TEXT, - credential_id INTEGER, - override_credential_username INTEGER, - vault_profile_id INTEGER, - enable_terminal INTEGER NOT NULL DEFAULT 1, - enable_session_logging INTEGER NOT NULL DEFAULT 1, - enable_command_history INTEGER NOT NULL DEFAULT 1, - enable_tunnel INTEGER NOT NULL DEFAULT 1, - tunnel_connections TEXT, - jump_hosts TEXT, - enable_file_manager INTEGER NOT NULL DEFAULT 1, - scp_legacy INTEGER NOT NULL DEFAULT 0, - enable_docker INTEGER NOT NULL DEFAULT 0, - enable_tmux_monitor INTEGER NOT NULL DEFAULT 0, - show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1, - show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0, - show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0, - default_path TEXT, - stats_config TEXT, - docker_config TEXT, - enable_proxmox INTEGER NOT NULL DEFAULT 0, - proxmox_config TEXT, - terminal_config TEXT, - quick_actions TEXT, - notes TEXT, - enable_ssh INTEGER NOT NULL DEFAULT 1, - enable_rdp INTEGER NOT NULL DEFAULT 0, - enable_vnc INTEGER NOT NULL DEFAULT 0, - enable_telnet INTEGER NOT NULL DEFAULT 0, - ssh_port INTEGER DEFAULT 22, - rdp_port INTEGER DEFAULT 3389, - vnc_port INTEGER DEFAULT 5900, - telnet_port INTEGER DEFAULT 23, - rdp_credential_id INTEGER, - rdp_user TEXT, - rdp_password TEXT, - rdp_domain TEXT, - rdp_security TEXT, - rdp_ignore_cert INTEGER DEFAULT 0, - vnc_credential_id INTEGER, - vnc_password TEXT, - vnc_user TEXT, - telnet_user TEXT, - telnet_password TEXT, - telnet_credential_id INTEGER, - rdp_auth_type TEXT, - vnc_auth_type TEXT, - telnet_auth_type TEXT, - domain TEXT, - security TEXT, - ignore_cert INTEGER DEFAULT 0, - guacamole_config TEXT, - use_socks5 INTEGER, - socks5_host TEXT, - socks5_port INTEGER, - socks5_username TEXT, - socks5_password TEXT, - socks5_proxy_chain TEXT, - mac_address TEXT, - wol_broadcast_address TEXT, - port_knock_sequence TEXT, - host_key_fingerprint TEXT, - host_key_type TEXT, - host_key_algorithm TEXT DEFAULT 'sha256', - host_key_first_seen TEXT, - host_key_last_verified TEXT, - host_key_changed_count INTEGER DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE ssh_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - folder TEXT, - tags TEXT, - auth_type TEXT NOT NULL, - username TEXT, - password TEXT, - key TEXT, - private_key TEXT, - public_key TEXT, - key_password TEXT, - key_type TEXT, - detected_key_type TEXT, - cert_public_key TEXT, - usage_count INTEGER NOT NULL DEFAULT 0, - last_used TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - - INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) - VALUES - (1, 'user-1', 'web', '10.0.0.1', 22, 'root', 'password'), - (2, 'user-2', 'db', '10.0.0.2', 22, 'root', 'password'); - INSERT INTO ssh_credentials (id, user_id, name, auth_type, username, password) VALUES (1, 'user-1', 'prod', 'password', 'root', 'secret'), (2, 'user-2', 'other', 'password', 'root', 'secret'); + INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) + VALUES + (1, 'user-1', 'web', '10.0.0.1', 22, 'root', 'password'), + (2, 'user-2', 'db', '10.0.0.2', 22, 'root', 'password'); `); return new UserDataExportRepository(context); diff --git a/src/backend/tests/database/repositories/user-preference-repository.test.ts b/src/backend/tests/database/repositories/user-preference-repository.test.ts index 3ff56e1..febf757 100644 --- a/src/backend/tests/database/repositories/user-preference-repository.test.ts +++ b/src/backend/tests/database/repositories/user-preference-repository.test.ts @@ -17,39 +17,7 @@ describe("UserPreferenceRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE user_preferences ( - user_id TEXT PRIMARY KEY, - reopen_tabs_on_login INTEGER NOT NULL DEFAULT 0, - theme TEXT, - font_size TEXT, - accent_color TEXT, - language TEXT, - storage_mode TEXT, - command_autocomplete INTEGER, - command_palette_enabled INTEGER, - show_host_tags INTEGER, - host_tray_on_click INTEGER, - pin_app_rail INTEGER, - expand_app_rail_on_hover INTEGER, - folders_collapsed INTEGER, - confirm_snippet_execution INTEGER, - disable_update_check INTEGER, - confirm_tab_close INTEGER, - hidden_rail_tabs TEXT, - compact_host_view INTEGER, - status_color_scheme TEXT, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'); `); diff --git a/src/backend/tests/database/repositories/user-repository.test.ts b/src/backend/tests/database/repositories/user-repository.test.ts new file mode 100644 index 0000000..5ca8315 --- /dev/null +++ b/src/backend/tests/database/repositories/user-repository.test.ts @@ -0,0 +1,95 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { UserRepository } from "../../../database/repositories/user-repository.js"; + +describe("UserRepository.listPage", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository(): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) VALUES + ('u1', 'alice', 'hash'), + ('u2', 'Bob', 'hash'), + ('u3', 'carol', 'hash'), + ('u4', 'dave', 'hash'), + ('u5', 'alicia', 'hash'); + `); + + return new UserRepository(context); + } + + it("returns a page ordered by username with the full total", async () => { + const repo = await createRepository(); + + const page = await repo.listPage({ limit: 2, offset: 0 }); + + expect(page.users.map((u) => u.username)).toEqual(["alice", "alicia"]); + // total counts every match, not just the returned page. + expect(page.total).toBe(5); + }); + + it("pages forward without repeating or skipping a row", async () => { + const repo = await createRepository(); + + const first = await repo.listPage({ limit: 2, offset: 0 }); + const second = await repo.listPage({ limit: 2, offset: 2 }); + const third = await repo.listPage({ limit: 2, offset: 4 }); + + expect( + [...first.users, ...second.users, ...third.users].map((u) => u.username), + ).toEqual(["alice", "alicia", "Bob", "carol", "dave"]); + }); + + it("filters by username case-insensitively", async () => { + const repo = await createRepository(); + + const page = await repo.listPage({ search: "ALI", limit: 10, offset: 0 }); + + expect(page.users.map((u) => u.username)).toEqual(["alice", "alicia"]); + expect(page.total).toBe(2); + }); + + it("counts only matching rows when searching", async () => { + const repo = await createRepository(); + + const page = await repo.listPage({ search: "ali", limit: 1, offset: 0 }); + + expect(page.users).toHaveLength(1); + expect(page.total).toBe(2); + }); + + it("returns an empty page for a term nobody matches", async () => { + const repo = await createRepository(); + + const page = await repo.listPage({ search: "zzz", limit: 10, offset: 0 }); + + expect(page.users).toEqual([]); + expect(page.total).toBe(0); + }); + + it("treats a blank search as no filter", async () => { + const repo = await createRepository(); + + const page = await repo.listPage({ search: " ", limit: 10, offset: 0 }); + + expect(page.total).toBe(5); + }); + + it("returns an empty page past the end of the results", async () => { + const repo = await createRepository(); + + const page = await repo.listPage({ limit: 10, offset: 99 }); + + expect(page.users).toEqual([]); + expect(page.total).toBe(5); + }); +}); diff --git a/src/backend/tests/database/repositories/user-session-repositories.test.ts b/src/backend/tests/database/repositories/user-session-repositories.test.ts index e26c508..aee820a 100644 --- a/src/backend/tests/database/repositories/user-session-repositories.test.ts +++ b/src/backend/tests/database/repositories/user-session-repositories.test.ts @@ -35,45 +35,6 @@ describe("UserRepository and SessionRepository", () => { }> { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - is_oidc INTEGER NOT NULL DEFAULT 0, - oidc_identifier TEXT, - sso_provider_id INTEGER, - client_id TEXT, - client_secret TEXT, - issuer_url TEXT, - authorization_url TEXT, - token_url TEXT, - identifier_path TEXT, - name_path TEXT, - scopes TEXT DEFAULT 'openid email profile', - totp_secret TEXT, - totp_enabled INTEGER NOT NULL DEFAULT 0, - totp_backup_codes TEXT, - registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - donation_modal_dismissed INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE sessions ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - jwt_token TEXT NOT NULL, - device_type TEXT NOT NULL, - device_info TEXT NOT NULL, - oidc_sub TEXT, - oidc_sid TEXT, - sso_provider_id INTEGER, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - last_active_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ); - `); return { users: new UserRepository(context, options.onUserWrite), @@ -232,6 +193,44 @@ describe("UserRepository and SessionRepository", () => { expect(await repo.sessions.findById("session-1")).toBeNull(); }); + it("persists session activity at most once per minute", async () => { + let writeCount = 0; + const repo = await createRepositories({ + onSessionWrite: () => { + writeCount += 1; + }, + }); + await repo.users.create({ + id: "user-1", + username: "user", + passwordHash: "hash", + isAdmin: false, + isOidc: false, + }); + await repo.sessions.create({ + id: "session-1", + userId: "user-1", + jwtToken: "token", + deviceType: "desktop", + deviceInfo: "Firefox", + createdAt: "2026-06-26T00:00:00.000Z", + expiresAt: "2026-06-27T00:00:00.000Z", + lastActiveAt: "2026-06-26T00:00:00.000Z", + }); + + expect( + await repo.sessions.touch("session-1", "2026-06-26T00:00:30.000Z"), + ).toBe(false); + expect( + await repo.sessions.touch("session-1", "2026-06-26T00:01:00.000Z"), + ).toBe(true); + + expect(writeCount).toBe(2); + expect((await repo.sessions.findById("session-1"))?.lastActiveAt).toBe( + "2026-06-26T00:01:00.000Z", + ); + }); + it("revokes all user sessions except an optional current session", async () => { const repo = await createRepositories(); await repo.users.create({ diff --git a/src/backend/tests/database/repositories/vault-profile-repository.test.ts b/src/backend/tests/database/repositories/vault-profile-repository.test.ts index 2c6cdfc..b5e9c4e 100644 --- a/src/backend/tests/database/repositories/vault-profile-repository.test.ts +++ b/src/backend/tests/database/repositories/vault-profile-repository.test.ts @@ -17,40 +17,13 @@ describe("VaultProfileRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE vault_profiles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - folder TEXT, - tags TEXT, - vault_addr TEXT NOT NULL, - vault_namespace TEXT, - oidc_mount TEXT, - oidc_role TEXT, - ssh_mount TEXT, - ssh_role TEXT NOT NULL, - valid_principals TEXT, - key_type TEXT, - shared INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); INSERT INTO vault_profiles ( id, user_id, name, vault_addr, ssh_role, shared, updated_at ) - VALUES - (1, 'user-1', 'owned', 'https://vault.one', 'role-one', 0, '2026-01-01T00:00:00.000Z'), + VALUES (1, 'user-1', 'owned', 'https://vault.one', 'role-one', 0, '2026-01-01T00:00:00.000Z'), (2, 'user-2', 'shared', 'https://vault.two', 'role-two', 1, '2026-01-02T00:00:00.000Z'), (3, 'user-2', 'hidden', 'https://vault.three', 'role-three', 0, '2026-01-03T00:00:00.000Z'); `); @@ -119,8 +92,8 @@ describe("VaultProfileRepository", () => { }); expect(await repo.updateById(999, { name: "missing" })).toBeNull(); - expect(await repo.deleteById(1)).toBe(true); - expect(await repo.deleteById(1)).toBe(false); + expect(await repo.deleteById(1)).toEqual({ syncId: null }); + expect(await repo.deleteById(1)).toBeNull(); expect(await repo.findById(1)).toBeNull(); expect(writeCount).toBe(2); }); diff --git a/src/backend/tests/database/repositories/vault-token-repository.test.ts b/src/backend/tests/database/repositories/vault-token-repository.test.ts index a67d104..01484f5 100644 --- a/src/backend/tests/database/repositories/vault-token-repository.test.ts +++ b/src/backend/tests/database/repositories/vault-token-repository.test.ts @@ -17,35 +17,11 @@ describe("VaultTokenRepository", () => { ): Promise { adapter = new TestSqliteDatabase(); const context = await adapter.connect(); - context.sqlite?.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password_hash TEXT NOT NULL - ); - - CREATE TABLE vault_profiles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - name TEXT NOT NULL - ); - - CREATE TABLE vault_tokens ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - profile_id INTEGER NOT NULL, - ssh_cert TEXT NOT NULL, - private_key TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - last_used TEXT, - UNIQUE(user_id, profile_id) - ); - + await adapter.exec(` INSERT INTO users (id, username, password_hash) VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO vault_profiles (id, user_id, name) - VALUES (1, 'user-1', 'one'), (2, 'user-2', 'two'); + INSERT INTO vault_profiles (id, user_id, name, vault_addr, ssh_role) + VALUES (1, 'user-1', 'one', 'http://vault', 'r'), (2, 'user-2', 'two', 'http://vault', 'r'); INSERT INTO vault_tokens ( user_id, profile_id, ssh_cert, private_key, expires_at ) diff --git a/src/backend/tests/database/repositories/workspace-repository.test.ts b/src/backend/tests/database/repositories/workspace-repository.test.ts new file mode 100644 index 0000000..e334e14 --- /dev/null +++ b/src/backend/tests/database/repositories/workspace-repository.test.ts @@ -0,0 +1,204 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TestSqliteDatabase } from "./test-support.js"; +import { WorkspaceRepository } from "../../../database/repositories/workspace-repository.js"; + +describe("WorkspaceRepository", () => { + let adapter: TestSqliteDatabase | null = null; + + afterEach(async () => { + if (adapter) { + await adapter.close(); + adapter = null; + } + }); + + async function createRepository( + onWrite?: () => void | Promise, + ): Promise { + adapter = new TestSqliteDatabase(); + const context = await adapter.connect(); + await adapter.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); + `); + + return new WorkspaceRepository(context, onWrite); + } + + it("creates and lists manual workspaces scoped to the owning user", async () => { + const repo = await createRepository(); + + await repo.create("user-1", { name: "Prod Debugging", payload: "{}" }); + await repo.create("user-2", { name: "Other user's", payload: "{}" }); + + const list = await repo.listByUser("user-1"); + expect(list).toHaveLength(1); + expect(list[0]).toMatchObject({ + userId: "user-1", + name: "Prod Debugging", + kind: "manual", + isDefault: false, + }); + expect(list[0].syncId).toBeTruthy(); + }); + + it("finds a workspace by id scoped to the owner", async () => { + const repo = await createRepository(); + const created = await repo.create("user-1", { + name: "Test A", + payload: "{}", + }); + + expect(await repo.findById("user-1", created.id)).toMatchObject({ + id: created.id, + }); + expect(await repo.findById("user-2", created.id)).toBeNull(); + }); + + it("upsertLastSession creates then updates a single row, never a second one", async () => { + const repo = await createRepository(); + + const first = await repo.upsertLastSession("user-1", '{"tabs":[]}'); + expect(first.kind).toBe("last_session"); + + const second = await repo.upsertLastSession( + "user-1", + '{"tabs":[{"slotId":"a"}]}', + ); + expect(second.id).toBe(first.id); + expect(second.payload).toBe('{"tabs":[{"slotId":"a"}]}'); + + const all = await repo.listByUser("user-1"); + expect(all.filter((w) => w.kind === "last_session")).toHaveLength(1); + }); + + it("update renames/recolors a manual workspace but rejects last_session", async () => { + const repo = await createRepository(); + const manual = await repo.create("user-1", { + name: "Old Name", + payload: "{}", + }); + const lastSession = await repo.upsertLastSession("user-1", "{}"); + + const updated = await repo.update("user-1", manual.id, { + name: "New Name", + color: "#fff", + }); + expect(updated).toMatchObject({ name: "New Name", color: "#fff" }); + + const rejected = await repo.update("user-1", lastSession.id, { + name: "Should not work", + }); + expect(rejected).toBeNull(); + }); + + it("updateContent overwrites payload for a manual workspace but rejects last_session", async () => { + const repo = await createRepository(); + const manual = await repo.create("user-1", { + name: "Test A", + payload: "{}", + }); + const lastSession = await repo.upsertLastSession("user-1", "{}"); + + const updated = await repo.updateContent( + "user-1", + manual.id, + '{"tabs":[1]}', + ); + expect(updated?.payload).toBe('{"tabs":[1]}'); + + const rejected = await repo.updateContent( + "user-1", + lastSession.id, + '{"tabs":[2]}', + ); + expect(rejected).toBeNull(); + }); + + it("setDefault clears any prior default and rejects last_session", async () => { + const repo = await createRepository(); + const a = await repo.create("user-1", { name: "A", payload: "{}" }); + const b = await repo.create("user-1", { name: "B", payload: "{}" }); + const lastSession = await repo.upsertLastSession("user-1", "{}"); + + await repo.setDefault("user-1", a.id); + expect((await repo.findById("user-1", a.id))?.isDefault).toBe(true); + + await repo.setDefault("user-1", b.id); + expect((await repo.findById("user-1", a.id))?.isDefault).toBe(false); + expect((await repo.findById("user-1", b.id))?.isDefault).toBe(true); + + const rejected = await repo.setDefault("user-1", lastSession.id); + expect(rejected).toBeNull(); + }); + + it("unsetDefault clears isDefault on a manual workspace and rejects last_session", async () => { + const repo = await createRepository(); + const a = await repo.create("user-1", { name: "A", payload: "{}" }); + const lastSession = await repo.upsertLastSession("user-1", "{}"); + + await repo.setDefault("user-1", a.id); + expect((await repo.findById("user-1", a.id))?.isDefault).toBe(true); + + await repo.unsetDefault("user-1", a.id); + expect((await repo.findById("user-1", a.id))?.isDefault).toBe(false); + + const rejected = await repo.unsetDefault("user-1", lastSession.id); + expect(rejected).toBeNull(); + }); + + it("touchLastUsed sets lastUsedAt", async () => { + const repo = await createRepository(); + const workspace = await repo.create("user-1", { + name: "A", + payload: "{}", + }); + expect(workspace.lastUsedAt).toBeNull(); + + await repo.touchLastUsed( + "user-1", + workspace.id, + "2026-08-11T00:00:00.000Z", + ); + expect((await repo.findById("user-1", workspace.id))?.lastUsedAt).toBe( + "2026-08-11T00:00:00.000Z", + ); + }); + + it("delete removes a manual workspace but rejects last_session", async () => { + const repo = await createRepository(); + const manual = await repo.create("user-1", { name: "A", payload: "{}" }); + const lastSession = await repo.upsertLastSession("user-1", "{}"); + + await expect(repo.delete("user-1", lastSession.id)).resolves.toBe(false); + await expect(repo.delete("user-1", manual.id)).resolves.toBe(true); + expect(await repo.findById("user-1", manual.id)).toBeNull(); + }); + + it("triggers writes on create/update/delete", async () => { + let writeCount = 0; + const repo = await createRepository(() => { + writeCount += 1; + }); + + const created = await repo.create("user-1", { + name: "A", + payload: "{}", + }); + await repo.update("user-1", created.id, { name: "B" }); + await repo.delete("user-1", created.id); + + expect(writeCount).toBe(3); + }); + + it("deleteByUserId removes every workspace owned by the user", async () => { + const repo = await createRepository(); + await repo.create("user-1", { name: "A", payload: "{}" }); + await repo.create("user-1", { name: "B", payload: "{}" }); + await repo.create("user-2", { name: "C", payload: "{}" }); + + await expect(repo.deleteByUserId("user-1")).resolves.toBe(2); + expect(await repo.listByUser("user-1")).toHaveLength(0); + expect(await repo.listByUser("user-2")).toHaveLength(1); + }); +}); diff --git a/src/backend/tests/database/routes/automations.test.ts b/src/backend/tests/database/routes/automations.test.ts new file mode 100644 index 0000000..90ab16c --- /dev/null +++ b/src/backend/tests/database/routes/automations.test.ts @@ -0,0 +1,511 @@ +import crypto from "node:crypto"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Request, Response, Router } from "express"; +import type { AutomationDefinition } from "../../../../types/automations.js"; + +/** + * Route-level behaviour: validation, ownership and webhook token handling. The repository and engine are mocked; what matters here is what + * the HTTP layer accepts, rejects and hands back. + */ + +const state = vi.hoisted(() => ({ + currentUserId: "user-1", + rows: [] as Array>, + nextId: 1, +})); + +vi.mock("../../../database/db/index.js", () => ({ db: {} })); + +vi.mock("../../../utils/logger.js", () => ({ + databaseLogger: { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + success: vi.fn(), + }, +})); + +const repository = vi.hoisted(() => ({ + list: vi.fn(), + findForUser: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + listAllEnabled: vi.fn(), + listRuns: vi.fn(), + findRunForUser: vi.fn(), + listRunSteps: vi.fn(), + upsertSchedule: vi.fn(), + deleteSchedule: vi.fn(), +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentAutomationRepository: () => repository, +})); + +const run = vi.hoisted(() => vi.fn()); +vi.mock("../../../automations/engine.js", () => ({ + AutomationEngine: { getInstance: () => ({ run }) }, +})); + +vi.mock("../../../utils/auth-manager.js", () => ({ + AuthManager: { + getInstance: () => ({ + createAuthMiddleware: + () => + (req: Record, _res: unknown, next: () => void) => { + req.userId = state.currentUserId; + next(); + }, + createDataAccessMiddleware: + () => (_req: unknown, _res: unknown, next: () => void) => + next(), + }), + }, +})); + +vi.mock("../../../utils/audit-logger.js", () => ({ + logAudit: vi.fn(async () => undefined), + getAuditUsername: vi.fn(async () => "alice"), + getRequestMeta: () => ({ ipAddress: "127.0.0.1", userAgent: "test" }), +})); + +const { default: router } = + await import("../../../database/routes/automations.js"); + +function findHandler(method: string, path: string) { + const stack = (router as unknown as Router).stack as Array<{ + route?: { + path: string; + methods: Record; + stack: Array<{ handle: (req: Request, res: Response) => unknown }>; + }; + }>; + const layer = stack.find( + (l) => l.route?.path === path && l.route?.methods[method], + ); + if (!layer?.route) throw new Error(`No route for ${method} ${path}`); + return layer.route.stack[layer.route.stack.length - 1].handle; +} + +/** Runs the whole middleware chain for the route, not just its handler. */ +async function invoke( + method: string, + path: string, + overrides: { + body?: Record; + params?: Record; + query?: Record; + } = {}, +) { + const stack = (router as unknown as Router).stack as Array<{ + route?: { + path: string; + methods: Record; + stack: Array<{ handle: (...args: unknown[]) => unknown }>; + }; + }>; + const layer = stack.find( + (l) => l.route?.path === path && l.route?.methods[method], + ); + if (!layer?.route) throw new Error(`No route for ${method} ${path}`); + + const req = { + userId: state.currentUserId, + body: overrides.body ?? {}, + params: overrides.params ?? {}, + query: overrides.query ?? {}, + headers: {}, + } as unknown as Request; + + const res = { + statusCode: 200, + jsonBody: null as unknown, + status(code: number) { + (this as unknown as { statusCode: number }).statusCode = code; + return this; + }, + json(payload: unknown) { + (this as unknown as { jsonBody: unknown }).jsonBody = payload; + return this; + }, + } as unknown as Response & { statusCode: number; jsonBody: unknown }; + + for (const entry of layer.route.stack) { + let advanced = false; + await entry.handle(req, res, () => { + advanced = true; + }); + if (!advanced) break; + } + + return res as unknown as { + statusCode: number; + jsonBody: Record | null; + }; +} + +function definition( + overrides: Partial = {}, +): AutomationDefinition { + return { + version: 1, + trigger: { + kind: "metric_threshold", + hostSelector: { kind: "host", hostId: 7 }, + metric: { path: "disk.percent", mount: "/data" }, + operator: ">", + value: 90, + cooldownMinutes: 15, + }, + steps: [{ id: "a", type: "notify", channelIds: [1] }], + ...overrides, + } as AutomationDefinition; +} + +beforeEach(() => { + state.currentUserId = "user-1"; + state.rows = []; + state.nextId = 1; + vi.clearAllMocks(); + + repository.list.mockImplementation(async (userId: string) => + state.rows.filter((row) => row.user_id === userId), + ); + repository.findForUser.mockImplementation( + async (id: number, userId: string) => + state.rows.find((row) => row.id === id && row.user_id === userId) ?? null, + ); + repository.create.mockImplementation( + async (input: Record) => { + const row = { + id: state.nextId++, + user_id: input.userId, + name: input.name, + definition: input.definition, + enabled: input.enabled === false ? 0 : 1, + channels: input.channels ?? [], + }; + state.rows.push(row); + return row; + }, + ); + repository.delete.mockImplementation(async (id: number, userId: string) => { + const index = state.rows.findIndex( + (row) => row.id === id && row.user_id === userId, + ); + if (index === -1) return false; + state.rows.splice(index, 1); + return true; + }); + repository.listAllEnabled.mockImplementation(async () => + state.rows.map((row) => ({ + id: row.id, + userId: row.user_id, + definition: row.definition, + enabled: true, + })), + ); + repository.upsertSchedule.mockResolvedValue(undefined); + repository.deleteSchedule.mockResolvedValue(undefined); + run.mockResolvedValue({ runId: 1, status: "success" }); +}); + +describe("POST /", () => { + it("creates an automation from a valid definition", async () => { + const res = await invoke("post", "/", { + body: { name: "Disk watch", definition: definition() }, + }); + + expect(res.statusCode).toBe(201); + expect(res.jsonBody?.name).toBe("Disk watch"); + }); + + it("requires a name", async () => { + const res = await invoke("post", "/", { + body: { definition: definition() }, + }); + expect(res.statusCode).toBe(400); + expect(String(res.jsonBody?.error)).toMatch(/name/i); + }); + + it("rejects an unknown trigger kind", async () => { + const res = await invoke("post", "/", { + body: { + name: "Bad", + definition: { version: 1, trigger: { kind: "nope" }, steps: [] }, + }, + }); + expect(res.statusCode).toBe(400); + expect(String(res.jsonBody?.error)).toMatch(/trigger/i); + }); + + it("rejects an unknown operator", async () => { + const res = await invoke("post", "/", { + body: { + name: "Bad", + definition: definition({ + trigger: { + kind: "metric_threshold", + hostSelector: { kind: "all" }, + metric: { path: "cpu.percent" }, + operator: "~=", + value: 1, + cooldownMinutes: 5, + }, + } as Partial), + }, + }); + expect(res.statusCode).toBe(400); + expect(String(res.jsonBody?.error)).toMatch(/operator/i); + }); + + it("rejects an unknown step type", async () => { + const res = await invoke("post", "/", { + body: { + name: "Bad", + definition: definition({ + steps: [{ id: "a", type: "launch_missiles" }], + } as Partial), + }, + }); + expect(res.statusCode).toBe(400); + expect(String(res.jsonBody?.error)).toMatch(/step type/i); + }); + + it("rejects duplicate step ids, including inside a branch", async () => { + const res = await invoke("post", "/", { + body: { + name: "Bad", + definition: definition({ + steps: [ + { id: "dup", type: "wait", seconds: 1 }, + { + id: "branch", + type: "if", + condition: { left: "1", operator: "==", right: "1" }, + then: [{ id: "dup", type: "wait", seconds: 1 }], + }, + ], + } as Partial), + }, + }); + expect(res.statusCode).toBe(400); + expect(String(res.jsonBody?.error)).toMatch(/duplicate/i); + }); + + it("rejects an invalid cron expression", async () => { + const res = await invoke("post", "/", { + body: { + name: "Bad", + definition: definition({ + trigger: { kind: "schedule", cron: "not a cron" }, + } as Partial), + }, + }); + expect(res.statusCode).toBe(400); + expect(String(res.jsonBody?.error)).toMatch(/cron/i); + }); + + it("rejects an interval under a minute", async () => { + const res = await invoke("post", "/", { + body: { + name: "Bad", + definition: definition({ + trigger: { kind: "schedule", intervalSeconds: 5 }, + } as Partial), + }, + }); + expect(res.statusCode).toBe(400); + expect(String(res.jsonBody?.error)).toMatch(/60 seconds/i); + }); + + it("registers a schedule for a schedule trigger", async () => { + await invoke("post", "/", { + body: { + name: "Nightly", + definition: definition({ + trigger: { kind: "schedule", cron: "0 2 * * *" }, + } as Partial), + }, + }); + expect(repository.upsertSchedule).toHaveBeenCalled(); + }); + + it("returns a webhook token once and stores only its hash", async () => { + const res = await invoke("post", "/", { + body: { + name: "Hooked", + definition: definition({ + trigger: { kind: "webhook", tokenHash: "" }, + } as Partial), + }, + }); + + expect(res.statusCode).toBe(201); + const token = res.jsonBody?.webhookToken as string; + expect(token).toMatch(/^[a-f0-9]{64}$/); + + const stored = JSON.parse(state.rows[0].definition as string); + expect(stored.trigger.tokenHash).not.toBe(token); + expect(stored.trigger.tokenHash).toBe( + crypto.createHash("sha256").update(token).digest("hex"), + ); + + // The hash is never echoed back to the client. + const body = res.jsonBody as { definition: AutomationDefinition }; + expect((body.definition.trigger as { tokenHash: string }).tokenHash).toBe( + "", + ); + }); + + it("stores the automation against the caller, not a supplied user id", async () => { + // Authorization here is ownership, the same as the other data routes: + // every read and write is scoped to req.userId, so a client cannot create + // an automation that belongs to somebody else. + await invoke("post", "/", { + body: { + name: "Mine", + definition: definition(), + userId: "user-2", + user_id: "user-2", + }, + }); + + expect(state.rows).toHaveLength(1); + expect(state.rows[0].user_id).toBe("user-1"); + }); +}); + +describe("GET /", () => { + it("only returns the caller's automations", async () => { + state.rows.push({ + id: 1, + user_id: "user-2", + name: "Theirs", + definition: JSON.stringify(definition()), + channels: [], + }); + + const res = await invoke("get", "/"); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toHaveLength(0); + }); +}); + +describe("DELETE /:id", () => { + it("will not delete another user's automation", async () => { + state.rows.push({ + id: 1, + user_id: "user-2", + name: "Theirs", + definition: JSON.stringify(definition()), + channels: [], + }); + + const res = await invoke("delete", "/:id", { params: { id: "1" } }); + expect(res.statusCode).toBe(404); + expect(state.rows).toHaveLength(1); + }); +}); + +describe("POST /:id/run", () => { + function seedOwned() { + state.rows.push({ + id: 1, + user_id: "user-1", + name: "Mine", + definition: JSON.stringify(definition()), + channels: [], + }); + } + + it("runs an owned automation", async () => { + seedOwned(); + const res = await invoke("post", "/:id/run", { params: { id: "1" } }); + + expect(res.statusCode).toBe(200); + expect(run).toHaveBeenCalledWith( + expect.objectContaining({ automationId: 1, triggerType: "manual" }), + ); + }); + + it("passes the dry-run flag through", async () => { + seedOwned(); + await invoke("post", "/:id/run", { + params: { id: "1" }, + body: { dryRun: true }, + }); + expect(run).toHaveBeenCalledWith(expect.objectContaining({ dryRun: true })); + }); + + it("refuses to run someone else's automation", async () => { + state.rows.push({ + id: 1, + user_id: "user-2", + name: "Theirs", + definition: JSON.stringify(definition()), + channels: [], + }); + + const res = await invoke("post", "/:id/run", { params: { id: "1" } }); + expect(res.statusCode).toBe(404); + expect(run).not.toHaveBeenCalled(); + }); +}); + +describe("POST /webhook/:token", () => { + function seedWebhook(token: string) { + state.rows.push({ + id: 1, + user_id: "user-1", + name: "Hooked", + definition: JSON.stringify( + definition({ + trigger: { + kind: "webhook", + tokenHash: crypto.createHash("sha256").update(token).digest("hex"), + }, + } as Partial), + ), + channels: [], + }); + } + + it("runs the automation matching the token", async () => { + const token = "a".repeat(64); + seedWebhook(token); + + const res = await invoke("post", "/webhook/:token", { + params: { token }, + body: { hello: "world" }, + }); + + expect(res.statusCode).toBe(202); + expect(run).toHaveBeenCalledWith( + expect.objectContaining({ automationId: 1, triggerType: "webhook" }), + ); + }); + + it("rejects a token that does not match", async () => { + seedWebhook("a".repeat(64)); + + const res = await invoke("post", "/webhook/:token", { + params: { token: "b".repeat(64) }, + }); + + expect(res.statusCode).toBe(404); + expect(run).not.toHaveBeenCalled(); + }); + + it("rejects a token too short to be real", async () => { + seedWebhook("a".repeat(64)); + + const res = await invoke("post", "/webhook/:token", { + params: { token: "short" }, + }); + + expect(res.statusCode).toBe(404); + expect(run).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/database/routes/desktop-auto-session.test.ts b/src/backend/tests/database/routes/desktop-auto-session.test.ts new file mode 100644 index 0000000..94ff837 --- /dev/null +++ b/src/backend/tests/database/routes/desktop-auto-session.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from "vitest"; +import type { Request } from "express"; +import type { UserRecord } from "../../../database/repositories/user-repository.js"; +import { + isLoopbackRequest, + extractBearerOrCookieToken, + resolveDesktopAutoSessionUser, +} from "../../../database/routes/desktop-auto-session.js"; + +function makeUser(overrides: Partial = {}): UserRecord { + return { + id: "user-1", + username: "local", + passwordHash: "", + isOidc: false, + totpEnabled: false, + isAdmin: false, + registeredAt: "2026-01-01T00:00:00.000Z", + ...overrides, + } as UserRecord; +} + +describe("isLoopbackRequest", () => { + it.each(["127.0.0.1", "::1", "::ffff:127.0.0.1"])( + "accepts %s as loopback", + (ip) => { + expect( + isLoopbackRequest({ + ip, + headers: {}, + socket: { remoteAddress: ip }, + } as unknown as Request), + ).toBe(true); + }, + ); + + it("accepts an IPv4-mapped loopback suffix", () => { + expect( + isLoopbackRequest({ + ip: "::ffff:127.0.0.1", + headers: {}, + socket: { remoteAddress: "::ffff:127.0.0.1" }, + } as unknown as Request), + ).toBe(true); + }); + + it("rejects a non-loopback TCP peer address", () => { + expect( + isLoopbackRequest({ + ip: "192.168.1.50", + headers: {}, + socket: { remoteAddress: "192.168.1.50" }, + } as unknown as Request), + ).toBe(false); + }); + + it("ignores a spoofed X-Forwarded-For value", () => { + expect( + isLoopbackRequest({ + ip: "127.0.0.1", + headers: { "x-forwarded-for": "127.0.0.1" }, + socket: { remoteAddress: "203.0.113.10" }, + } as unknown as Request), + ).toBe(false); + }); + + it("rejects requests that traversed the reverse proxy (X-Real-IP set)", () => { + expect( + isLoopbackRequest({ + ip: "127.0.0.1", + headers: { "x-real-ip": "203.0.113.10" }, + socket: { remoteAddress: "127.0.0.1" }, + } as unknown as Request), + ).toBe(false); + }); +}); + +describe("extractBearerOrCookieToken", () => { + it("prefers the jwt cookie over the Authorization header", () => { + const req = { + cookies: { jwt: "cookie-token" }, + headers: { authorization: "Bearer header-token" }, + } as unknown as Request; + expect(extractBearerOrCookieToken(req)).toBe("cookie-token"); + }); + + it("falls back to a Bearer Authorization header", () => { + const req = { + cookies: {}, + headers: { authorization: "Bearer header-token" }, + } as unknown as Request; + expect(extractBearerOrCookieToken(req)).toBe("header-token"); + }); + + it("returns undefined when neither is present", () => { + const req = { cookies: {}, headers: {} } as unknown as Request; + expect(extractBearerOrCookieToken(req)).toBeUndefined(); + }); + + it("ignores a non-Bearer Authorization header", () => { + const req = { + cookies: {}, + headers: { authorization: "Basic abc123" }, + } as unknown as Request; + expect(extractBearerOrCookieToken(req)).toBeUndefined(); + }); +}); + +describe("resolveDesktopAutoSessionUser", () => { + it("returns the sole local user regardless of having a real password", () => { + const user = makeUser({ passwordHash: "$2a$10$realbcryptvaluehere" }); + expect(resolveDesktopAutoSessionUser([user])).toBe(user); + }); + + it("returns the sole local user even when OIDC-enabled", () => { + const user = makeUser({ isOidc: true }); + expect(resolveDesktopAutoSessionUser([user])).toBe(user); + }); + + it("returns the sole local user even when TOTP-enabled", () => { + const user = makeUser({ totpEnabled: true }); + expect(resolveDesktopAutoSessionUser([user])).toBe(user); + }); + + it("returns the auto-provisioned passwordless placeholder", () => { + const user = makeUser({ passwordHash: "" }); + expect(resolveDesktopAutoSessionUser([user])).toBe(user); + }); + + it("declines when zero users exist", () => { + expect(resolveDesktopAutoSessionUser([])).toBeNull(); + }); + + it("never declines for a multi-user local database -- prefers the admin account", () => { + const admin = makeUser({ + id: "user-2", + isAdmin: true, + registeredAt: "2026-02-01T00:00:00.000Z", + }); + const result = resolveDesktopAutoSessionUser([ + makeUser({ + id: "user-1", + isAdmin: false, + registeredAt: "2026-01-01T00:00:00.000Z", + }), + admin, + ]); + expect(result).toBe(admin); + }); + + it("falls back to the earliest-registered account when no admin exists", () => { + const earliest = makeUser({ + id: "user-1", + registeredAt: "2026-01-01T00:00:00.000Z", + }); + const result = resolveDesktopAutoSessionUser([ + makeUser({ id: "user-2", registeredAt: "2026-03-01T00:00:00.000Z" }), + earliest, + makeUser({ id: "user-3", registeredAt: "2026-02-01T00:00:00.000Z" }), + ]); + expect(result).toBe(earliest); + }); +}); diff --git a/src/backend/tests/database/routes/fleet-routes.test.ts b/src/backend/tests/database/routes/fleet-routes.test.ts new file mode 100644 index 0000000..5f97b18 --- /dev/null +++ b/src/backend/tests/database/routes/fleet-routes.test.ts @@ -0,0 +1,435 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { Request, Response, Router } from "express"; + +const state = vi.hoisted(() => ({ + currentUserId: "user-1", + fleets: new Map(), + members: new Map(), + hostAccess: new Map(), + hosts: new Map>(), +})); + +vi.mock("../../../database/db/index.js", () => ({ db: {} })); + +vi.mock("../../../utils/logger.js", () => ({ + authLogger: { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + success: vi.fn(), + }, + databaseLogger: { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + success: vi.fn(), + }, +})); + +vi.mock("../../../utils/auth-manager.js", () => ({ + AuthManager: { + getInstance: () => ({ + createAuthMiddleware: + () => + (req: Record, _res: unknown, next: () => void) => { + req.userId = state.currentUserId; + next(); + }, + createDataAccessMiddleware: + () => (_req: unknown, _res: unknown, next: () => void) => + next(), + }), + }, +})); + +vi.mock("../../../utils/permission-manager.js", () => ({ + PermissionManager: { + getInstance: () => ({ + canAccessHost: async (userId: string, hostId: number, level: string) => { + const key = `${userId}:${hostId}:${level}`; + const found = state.hostAccess.get(key); + if (found) return found; + // default: full access unless a test explicitly denies it + return { hasAccess: true, isOwner: true, permissionLevel: "manage" }; + }, + }), + }, +})); + +vi.mock("../../../hosts/host-resolver.js", () => ({ + resolveHostById: async (hostId: number) => state.hosts.get(hostId) ?? null, +})); + +vi.mock("../../../hosts/ssh-client-factory.js", () => ({ + getFleetPoolKey: () => "pool-key", + createFleetSshFactory: () => async () => ({}), +})); + +vi.mock("../../../hosts/ssh-connection-pool.js", () => ({ + withConnection: async ( + _key: string, + _factory: unknown, + fn: (client: unknown) => unknown, + ) => fn({}), +})); + +vi.mock("../../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: vi.fn(async () => ({ stdout: "ok", stderr: "", code: 0 })), +})); + +vi.mock("../../../hosts/metrics/managers/platform.js", () => ({ + detectPlatform: vi.fn(async () => ({ pkg: "apt", osPrettyName: "Debian" })), +})); + +vi.mock("../../../hosts/metrics/managers/exec-elevated.js", async () => { + class ElevationError extends Error { + code: string; + constructor(code: string, message: string) { + super(message); + this.code = code; + } + } + return { + execElevated: vi.fn(), + ElevationError, + }; +}); + +vi.mock("../../../hosts/metrics/managers/packages.js", () => ({ + buildPackageActionCommand: vi.fn(() => "apt-get install -y foo"), +})); + +vi.mock("../../../hosts/metrics/managers/validation.js", () => ({ + isValidPackageName: (v: unknown) => typeof v === "string" && v.length > 0, +})); + +vi.mock("../../../database/routes/snippets-execution.js", () => ({ + resolveSnippetCommand: (command: string) => command, +})); + +vi.mock("../../../database/routes/rbac.js", () => ({ + isSharePermissionLevel: (v: unknown) => + ["connect", "view", "edit", "manage"].includes(v as string), + expiryFromDuration: () => null, + parseShareTargets: () => null, +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentFleetRepository: () => ({ + findById: async (userId: string, fleetId: number) => { + const fleet = state.fleets.get(fleetId); + return fleet && fleet.userId === userId ? fleet : null; + }, + listEffectiveMembers: async (_userId: string, fleetId: number) => + state.members.get(fleetId) ?? [], + listStaticMemberIds: async () => [], + listByUser: async (userId: string) => + [...state.fleets.values()].filter((f) => f.userId === userId), + }), + createCurrentFleetInventoryRepository: () => ({ + listForHosts: async () => [], + upsert: async () => ({}), + }), + createCurrentRbacAccessRepository: () => ({}), + createCurrentRoleRepository: () => ({}), + createCurrentUserRepository: () => ({}), +})); + +const { + default: router, + parseInventoryProbe, + buildRemoveCommand, +} = await import("../../../database/routes/fleet-routes.js"); + +function findLayer(method: string, path: string) { + const stack = (router as unknown as Router).stack as Array<{ + route?: { + path: string; + methods: Record; + stack: Array<{ handle: (req: Request, res: Response) => unknown }>; + }; + }>; + const layer = stack.find( + (l) => l.route?.path === path && l.route?.methods[method], + ); + if (!layer?.route) throw new Error(`No route for ${method} ${path}`); + return layer.route.stack[layer.route.stack.length - 1].handle; +} + +function makeReqRes(overrides: { + body?: Record; + params?: Record; +}) { + const req = { + userId: state.currentUserId, + body: overrides.body ?? {}, + params: overrides.params ?? {}, + headers: {}, + } as unknown as Request; + + const res = { + statusCode: 200, + jsonBody: null as unknown, + status(code: number) { + (this as unknown as { statusCode: number }).statusCode = code; + return this; + }, + json(payload: unknown) { + (this as unknown as { jsonBody: unknown }).jsonBody = payload; + return this; + }, + } as unknown as Response & { statusCode: number; jsonBody: unknown }; + + return { req, res }; +} + +async function invoke( + method: string, + path: string, + overrides: { + body?: Record; + params?: Record; + } = {}, +) { + const handler = findLayer(method, path); + const { req, res } = makeReqRes(overrides); + await handler(req, res); + return res as unknown as { + statusCode: number; + jsonBody: Record | null; + }; +} + +beforeEach(() => { + state.currentUserId = "user-1"; + state.fleets = new Map([[1, { id: 1, userId: "user-1", name: "web fleet" }]]); + state.members = new Map([ + [ + 1, + [ + { id: 10, name: "host-a" }, + { id: 11, name: "host-b" }, + ], + ], + ]); + state.hostAccess = new Map(); + state.hosts = new Map([ + [ + 10, + { + id: 10, + userId: "user-1", + name: "host-a", + ip: "10.0.0.1", + port: 22, + username: "root", + sudoPassword: "secret", + }, + ], + [ + 11, + { + id: 11, + userId: "user-1", + name: "host-b", + ip: "10.0.0.2", + port: 22, + username: "root", + sudoPassword: "secret", + }, + ], + ]); +}); + +describe("GET /:id/members", () => { + it("404s for a fleet the caller does not own", async () => { + const res = await invoke("get", "/:id/members", { + params: { id: "999" }, + }); + expect(res.statusCode).toBe(404); + }); + + it("400s on a non-numeric fleet id", async () => { + const res = await invoke("get", "/:id/members", { + params: { id: "not-a-number" }, + }); + expect(res.statusCode).toBe(400); + }); +}); + +describe("POST /:id/execute", () => { + it("400s when command is missing", async () => { + const res = await invoke("post", "/:id/execute", { + params: { id: "1" }, + body: {}, + }); + expect(res.statusCode).toBe(400); + }); + + it("reports per-host success with the {hostId, hostName, success} shape", async () => { + const res = await invoke("post", "/:id/execute", { + params: { id: "1" }, + body: { command: "uptime" }, + }); + expect(res.statusCode).toBe(200); + const results = res.jsonBody?.results as Array>; + expect(results).toHaveLength(2); + expect(results[0]).toMatchObject({ + hostId: 10, + hostName: "host-a", + success: true, + }); + }); + + it("isolates one host's access denial - the other host still succeeds", async () => { + state.hostAccess.set("user-1:10:edit", { + hasAccess: false, + isOwner: false, + }); + + const res = await invoke("post", "/:id/execute", { + params: { id: "1" }, + body: { command: "uptime" }, + }); + + const results = res.jsonBody?.results as Array>; + const denied = results.find((r) => r.hostId === 10); + const allowed = results.find((r) => r.hostId === 11); + expect(denied).toMatchObject({ success: false }); + expect(String(denied?.error)).toMatch(/edit/); + expect(allowed).toMatchObject({ success: true }); + }); + + it("404s for a fleet the caller does not own", async () => { + const res = await invoke("post", "/:id/execute", { + params: { id: "999" }, + body: { command: "uptime" }, + }); + expect(res.statusCode).toBe(404); + }); +}); + +describe("POST /:id/packages", () => { + it("400s on an invalid action", async () => { + const res = await invoke("post", "/:id/packages", { + params: { id: "1" }, + body: { action: "reformat-disk" }, + }); + expect(res.statusCode).toBe(400); + }); + + it("surfaces an ElevationError as a per-host error, not a request failure", async () => { + const { execElevated, ElevationError } = + await import("../../../hosts/metrics/managers/exec-elevated.js"); + (execElevated as ReturnType).mockRejectedValue( + new ElevationError("SUDO_REQUIRED", "sudo password required"), + ); + + const res = await invoke("post", "/:id/packages", { + params: { id: "1" }, + body: { action: "install", package: "curl" }, + }); + + expect(res.statusCode).toBe(200); + const results = res.jsonBody?.results as Array>; + expect(results.every((r) => r.success === false)).toBe(true); + expect(results[0].error).toMatch(/sudo password required/); + }); + + it("requires manage-level access, not just edit", async () => { + state.hostAccess.set("user-1:10:manage", { + hasAccess: false, + isOwner: false, + }); + state.hostAccess.set("user-1:11:manage", { + hasAccess: false, + isOwner: false, + }); + + const { execElevated } = + await import("../../../hosts/metrics/managers/exec-elevated.js"); + (execElevated as ReturnType).mockResolvedValue({ + code: 0, + stdout: "done", + stderr: "", + }); + + const res = await invoke("post", "/:id/packages", { + params: { id: "1" }, + body: { action: "upgrade-all" }, + }); + + const results = res.jsonBody?.results as Array>; + expect(results.every((r) => r.success === false)).toBe(true); + expect(String(results[0].error)).toMatch(/manage/); + }); +}); + +describe("POST /:id/members", () => { + it("404s when the caller cannot access the target host", async () => { + state.hostAccess.set("user-1:99:connect", { + hasAccess: false, + isOwner: false, + }); + + const res = await invoke("post", "/:id/members", { + params: { id: "1" }, + body: { hostId: 99 }, + }); + expect(res.statusCode).toBe(404); + }); +}); + +describe("parseInventoryProbe", () => { + it("extracts kernel, arch, hostname, and uptime from key=value lines", () => { + const out = [ + "kernel=6.1.0-generic", + "arch=x86_64", + "hostname=web-1", + "uptime_seconds=123456", + ].join("\n"); + + expect(parseInventoryProbe(out)).toEqual({ + kernel: "6.1.0-generic", + architecture: "x86_64", + hostname: "web-1", + uptimeSeconds: 123456, + }); + }); + + it("nulls out fields missing from the probe output", () => { + expect(parseInventoryProbe("kernel=6.1.0")).toEqual({ + kernel: "6.1.0", + architecture: null, + hostname: null, + uptimeSeconds: null, + }); + }); + + it("nulls uptimeSeconds when the value is not a bare integer", () => { + const out = "uptime_seconds="; + expect(parseInventoryProbe(out).uptimeSeconds).toBeNull(); + }); + + it("ignores lines with no '=' separator", () => { + const out = ["garbage line", "kernel=6.1.0"].join("\n"); + expect(parseInventoryProbe(out).kernel).toBe("6.1.0"); + }); +}); + +describe("buildRemoveCommand", () => { + it("builds the correct remove command per package manager", () => { + expect(buildRemoveCommand("apt", "curl")).toContain( + "apt-get -y remove curl", + ); + expect(buildRemoveCommand("dnf", "curl")).toBe("dnf -y remove curl"); + expect(buildRemoveCommand("yum", "curl")).toBe("yum -y remove curl"); + expect(buildRemoveCommand("pacman", "curl")).toBe( + "pacman -R --noconfirm curl", + ); + }); + + it("returns null when no package manager was detected", () => { + expect(buildRemoveCommand(null, "curl")).toBeNull(); + }); +}); diff --git a/src/backend/tests/database/routes/host-normalizers.test.ts b/src/backend/tests/database/routes/host-normalizers.test.ts index eb0a863..c489cc4 100644 --- a/src/backend/tests/database/routes/host-normalizers.test.ts +++ b/src/backend/tests/database/routes/host-normalizers.test.ts @@ -1,6 +1,8 @@ import { describe, it, expect } from "vitest"; import { + containsOwnerPrivateAuthUpdate, isNonEmptyString, + isOptionalBoolean, isValidPort, normalizeImportedHost, renameFolderPath, @@ -9,6 +11,45 @@ import { transformHostResponse, } from "../../../database/routes/host-normalizers.js"; +describe("containsOwnerPrivateAuthUpdate", () => { + it("detects owner-only SSH auth fields, including explicit clears", () => { + expect(containsOwnerPrivateAuthUpdate({ password: null }, "ssh")).toBe( + true, + ); + expect( + containsOwnerPrivateAuthUpdate({ credentialId: undefined }, "ssh"), + ).toBe(true); + expect( + containsOwnerPrivateAuthUpdate({ authType: "password" }, "ssh"), + ).toBe(true); + expect(containsOwnerPrivateAuthUpdate({ shareSshAuth: true }, "ssh")).toBe( + true, + ); + }); + + it("keeps protocol field definitions isolated", () => { + expect(containsOwnerPrivateAuthUpdate({ rdpCredentialId: 7 }, "rdp")).toBe( + true, + ); + expect(containsOwnerPrivateAuthUpdate({ rdpCredentialId: 7 }, "ssh")).toBe( + false, + ); + }); + + it("allows shared editors to update non-authentication host settings", () => { + expect( + containsOwnerPrivateAuthUpdate( + { + name: "renamed", + ip: "10.0.0.5", + notes: "updated", + }, + "ssh", + ), + ).toBe(false); + }); +}); + describe("isNonEmptyString", () => { it("accepts non-blank strings", () => { expect(isNonEmptyString("hello")).toBe(true); @@ -24,6 +65,21 @@ describe("isNonEmptyString", () => { }); }); +describe("isOptionalBoolean", () => { + it("accepts booleans and an omitted value", () => { + expect(isOptionalBoolean(true)).toBe(true); + expect(isOptionalBoolean(false)).toBe(true); + expect(isOptionalBoolean(undefined)).toBe(true); + }); + + it("rejects truthy string and numeric lookalikes", () => { + expect(isOptionalBoolean("false")).toBe(false); + expect(isOptionalBoolean("0")).toBe(false); + expect(isOptionalBoolean(1)).toBe(false); + expect(isOptionalBoolean(null)).toBe(false); + }); +}); + describe("renameFolderPath", () => { it("renames an exact folder match", () => { expect(renameFolderPath("Production", "Production", "Prod")).toBe("Prod"); @@ -143,11 +199,16 @@ describe("stripSensitiveFields", () => { key: "PRIVATE KEY", keyPassword: "kp", sudoPassword: "sp", + terminalConfig: { + theme: "termix", + sudoPassword: "nested-sudo", + }, }); expect(result.password).toBeUndefined(); expect(result.key).toBeUndefined(); expect(result.keyPassword).toBeUndefined(); expect(result.sudoPassword).toBeUndefined(); + expect(result.terminalConfig).toEqual({ theme: "termix" }); expect(result.hasPassword).toBe(true); expect(result.hasKey).toBe(true); expect(result.hasKeyPassword).toBe(true); @@ -160,6 +221,42 @@ describe("stripSensitiveFields", () => { expect(result.hasPassword).toBe(false); expect(result.hasKey).toBe(false); }); + + it("detects sudo password stored only in nested terminalConfig", () => { + const result = stripSensitiveFields({ + name: "web", + terminalConfig: { + theme: "termix", + sudoPassword: "nested-only-sudo", + }, + }); + expect(result.hasSudoPassword).toBe(true); + expect( + (result.terminalConfig as Record).sudoPassword, + ).toBeUndefined(); + }); + + it("strips rdp/vnc/telnet passwords and adds their presence flags", () => { + const result = stripSensitiveFields({ + name: "rdp-box", + rdpPassword: "rdp-secret", + vncPassword: "vnc-secret", + telnetPassword: "telnet-secret", + }); + expect(result.rdpPassword).toBeUndefined(); + expect(result.vncPassword).toBeUndefined(); + expect(result.telnetPassword).toBeUndefined(); + expect(result.hasRdpPassword).toBe(true); + expect(result.hasVncPassword).toBe(true); + expect(result.hasTelnetPassword).toBe(true); + }); + + it("marks rdp/vnc/telnet presence flags false when absent", () => { + const result = stripSensitiveFields({ name: "rdp-box" }); + expect(result.hasRdpPassword).toBe(false); + expect(result.hasVncPassword).toBe(false); + expect(result.hasTelnetPassword).toBe(false); + }); }); describe("transformHostResponse", () => { @@ -168,11 +265,13 @@ describe("transformHostResponse", () => { tags: "a,b,c", enableTerminal: 1, enableTunnel: 0, + shareSshAuth: 1, pin: 1, }); expect(result.tags).toEqual(["a", "b", "c"]); expect(result.enableTerminal).toBe(true); expect(result.enableTunnel).toBe(false); + expect(result.shareSshAuth).toBe(true); expect(result.pin).toBe(true); }); @@ -233,9 +332,13 @@ describe("sanitizeHostForRecipient", () => { port: 22, username: "root", folder: "servers", + parentHostId: 17, tags: ["linux"], notes: "secret runbook", quickActions: [{ name: "restart", snippetId: "1" }], + credentialId: 7, + shareSshAuth: true, + overrideCredentialUsername: true, password: "hunter2", key: "PRIVATE", sudoPassword: "sudo", @@ -246,6 +349,11 @@ describe("sanitizeHostForRecipient", () => { sshPort: 22, rdpPort: 3389, defaultPath: "/srv", + terminalConfig: { + theme: "termix", + sudoPassword: "nested-sudo", + agentSocketPath: "/run/user/1000/ssh-agent.sock", + }, }; it("always strips secrets for recipients", () => { @@ -255,14 +363,45 @@ describe("sanitizeHostForRecipient", () => { expect(result.sudoPassword).toBeUndefined(); expect(result.rdpPassword).toBeUndefined(); expect(result.socks5Password).toBeUndefined(); + expect(result.credentialId).toBeUndefined(); + expect(result.overrideCredentialUsername).toBeUndefined(); + expect(result.terminalConfig).toEqual({ theme: "termix" }); + expect(result.shareSshAuth).toBe(true); + expect(result.hasPassword).toBe(false); + expect(result.hasKey).toBe(false); // view keeps configuration fields expect(result.notes).toBe("secret runbook"); expect(result.quickActions).toEqual(sharedHost.quickActions); }); + it("never exposes parentHostId to a recipient, at any permission level", () => { + // A recipient generally can't see (or share-permission on) the owner's + // parent host row, so sub-host tree structure is never leaked -- a + // shared host always renders at root for its recipient. + expect( + sanitizeHostForRecipient({ ...sharedHost }, "view").parentHostId, + ).toBeUndefined(); + expect( + sanitizeHostForRecipient({ ...sharedHost }, "manage").parentHostId, + ).toBeUndefined(); + expect( + sanitizeHostForRecipient({ ...sharedHost }, "connect").parentHostId, + ).toBeUndefined(); + }); + it("reduces connect-level hosts to connection essentials", () => { const result = sanitizeHostForRecipient( - { ...sharedHost, permissionLevel: "connect" }, + { + ...sharedHost, + permissionLevel: "connect", + authOverrides: { + ssh: { + credentialId: 9, + required: false, + ownerAuthShared: true, + }, + }, + }, "connect", ); expect(result.name).toBe("prod"); @@ -270,6 +409,14 @@ describe("sanitizeHostForRecipient", () => { expect(result.enableRdp).toBe(true); expect(result.rdpPort).toBe(3389); expect(result.permissionLevel).toBe("connect"); + expect(result.shareSshAuth).toBe(true); + expect(result.authOverrides).toEqual({ + ssh: { + credentialId: 9, + required: false, + ownerAuthShared: true, + }, + }); expect(result.notes).toBeUndefined(); expect(result.quickActions).toBeUndefined(); expect(result.password).toBeUndefined(); diff --git a/src/backend/tests/database/routes/host-parent-validation.test.ts b/src/backend/tests/database/routes/host-parent-validation.test.ts new file mode 100644 index 0000000..633cfa0 --- /dev/null +++ b/src/backend/tests/database/routes/host-parent-validation.test.ts @@ -0,0 +1,86 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const listOwnHostParentLinks = vi.fn(); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentHostResolutionRepository: () => ({ + listOwnHostParentLinks, + }), +})); + +describe("validateParentHostId", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("rejects a host being set as its own parent", async () => { + const { validateParentHostId } = + await import("../../../database/routes/host-parent-validation.js"); + + const error = await validateParentHostId("user-1", 5, 5); + expect(error).toMatch(/own parent/); + expect(listOwnHostParentLinks).not.toHaveBeenCalled(); + }); + + it("rejects a parent host that doesn't belong to the user", async () => { + listOwnHostParentLinks.mockResolvedValue([ + { id: 1, parentHostId: null }, + { id: 2, parentHostId: null }, + ]); + const { validateParentHostId } = + await import("../../../database/routes/host-parent-validation.js"); + + const error = await validateParentHostId("user-1", 1, 99); + expect(error).toMatch(/not found/); + }); + + it("accepts a valid, cycle-free parent assignment", async () => { + listOwnHostParentLinks.mockResolvedValue([ + { id: 1, parentHostId: null }, + { id: 2, parentHostId: null }, + ]); + const { validateParentHostId } = + await import("../../../database/routes/host-parent-validation.js"); + + const error = await validateParentHostId("user-1", 2, 1); + expect(error).toBeNull(); + }); + + it("rejects assigning a host under its own descendant (direct cycle)", async () => { + // Zeus (1) currently has VM (2) as a child; assigning Zeus under VM + // would form a two-node cycle. + listOwnHostParentLinks.mockResolvedValue([ + { id: 1, parentHostId: null }, + { id: 2, parentHostId: 1 }, + ]); + const { validateParentHostId } = + await import("../../../database/routes/host-parent-validation.js"); + + const error = await validateParentHostId("user-1", 1, 2); + expect(error).toMatch(/descendant/); + }); + + it("rejects assigning a host under a deeper descendant (multi-level cycle)", async () => { + // Zeus (1) -> VM (2) -> Nested (3); assigning Zeus under Nested must + // also be rejected, not just the direct-child case. + listOwnHostParentLinks.mockResolvedValue([ + { id: 1, parentHostId: null }, + { id: 2, parentHostId: 1 }, + { id: 3, parentHostId: 2 }, + ]); + const { validateParentHostId } = + await import("../../../database/routes/host-parent-validation.js"); + + const error = await validateParentHostId("user-1", 1, 3); + expect(error).toMatch(/descendant/); + }); + + it("allows a create (no existing hostId) to target any owned host", async () => { + listOwnHostParentLinks.mockResolvedValue([{ id: 1, parentHostId: null }]); + const { validateParentHostId } = + await import("../../../database/routes/host-parent-validation.js"); + + const error = await validateParentHostId("user-1", null, 1); + expect(error).toBeNull(); + }); +}); diff --git a/src/backend/tests/database/routes/keybinding-validation.test.ts b/src/backend/tests/database/routes/keybinding-validation.test.ts new file mode 100644 index 0000000..0802be3 --- /dev/null +++ b/src/backend/tests/database/routes/keybinding-validation.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect } from "vitest"; +import { + isValidKeyCombo, + isValidKeybindingAction, + isValidKeybinding, +} from "../../../database/routes/keybinding-validation.js"; + +const validCombo = { + key: "c", + isCode: false, + ctrl: true, + alt: false, + shift: false, + meta: false, +}; + +describe("isValidKeyCombo", () => { + it("accepts a well-formed combo", () => { + expect(isValidKeyCombo(validCombo)).toBe(true); + }); + + it("rejects a combo missing a boolean field", () => { + const { ctrl: _ctrl, ...rest } = validCombo; + expect(isValidKeyCombo(rest)).toBe(false); + }); + + it("rejects a non-object", () => { + expect(isValidKeyCombo("ctrl+c")).toBe(false); + expect(isValidKeyCombo(null)).toBe(false); + }); +}); + +describe("isValidKeybindingAction", () => { + it("accepts copy and paste with no extra fields", () => { + expect(isValidKeybindingAction({ type: "copy" })).toBe(true); + expect(isValidKeybindingAction({ type: "paste" })).toBe(true); + }); + + it("rejects an unknown action type", () => { + expect(isValidKeybindingAction({ type: "explode" })).toBe(false); + }); + + it("requires text for sendText", () => { + expect(isValidKeybindingAction({ type: "sendText" })).toBe(false); + expect(isValidKeybindingAction({ type: "sendText", text: "ls -la" })).toBe( + true, + ); + }); + + it("requires a single-letter controlCode for sendControlCode", () => { + expect( + isValidKeybindingAction({ type: "sendControlCode", controlCode: "w" }), + ).toBe(true); + expect( + isValidKeybindingAction({ type: "sendControlCode", controlCode: "ww" }), + ).toBe(false); + expect( + isValidKeybindingAction({ type: "sendControlCode", controlCode: "1" }), + ).toBe(false); + expect(isValidKeybindingAction({ type: "sendControlCode" })).toBe(false); + }); + + it("requires snippetId for runSnippet", () => { + expect( + isValidKeybindingAction({ type: "runSnippet", snippetId: "42" }), + ).toBe(true); + expect(isValidKeybindingAction({ type: "runSnippet" })).toBe(false); + }); +}); + +describe("isValidKeybinding", () => { + const base = { + id: "kb-1", + enabled: true, + combo: validCombo, + action: { type: "copy" }, + }; + + it("accepts a well-formed keybinding", () => { + expect(isValidKeybinding(base)).toBe(true); + }); + + it("rejects a keybinding missing id", () => { + const { id: _id, ...rest } = base; + expect(isValidKeybinding(rest)).toBe(false); + }); + + it("rejects a keybinding missing enabled", () => { + const { enabled: _enabled, ...rest } = base; + expect(isValidKeybinding(rest)).toBe(false); + }); + + it("rejects a keybinding with an invalid combo", () => { + expect(isValidKeybinding({ ...base, combo: {} })).toBe(false); + }); + + it("rejects a keybinding with an invalid action", () => { + expect(isValidKeybinding({ ...base, action: { type: "sendText" } })).toBe( + false, + ); + }); +}); diff --git a/src/backend/tests/database/routes/proxmox-import-auth.test.ts b/src/backend/tests/database/routes/proxmox-import-auth.test.ts new file mode 100644 index 0000000..0d40493 --- /dev/null +++ b/src/backend/tests/database/routes/proxmox-import-auth.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { resolveProxmoxImportAuth } from "../../../database/routes/proxmox-import-auth.js"; + +// The frontend carries its own copy of this decision in +// src/ui/components/proxmox/proxmox-import-auth.ts. The two drifting apart is +// what produced the reported bug, so both are held to the same matrix. +describe("resolveProxmoxImportAuth", () => { + it("uses the default credential for key auth when one is configured", () => { + expect(resolveProxmoxImportAuth("key", 7)).toEqual({ + authType: "credential", + credentialId: 7, + overrideCredentialUsername: 1, + }); + }); + + it("uses the default credential for password auth when one is configured", () => { + expect(resolveProxmoxImportAuth("password", 7)).toEqual({ + authType: "credential", + credentialId: 7, + overrideCredentialUsername: 1, + }); + }); + + it("falls back to none when a secret-backed default has no credential", () => { + for (const authType of ["password", "key", "credential"]) { + expect(resolveProxmoxImportAuth(authType, null)).toEqual({ + authType: "none", + credentialId: null, + overrideCredentialUsername: 0, + }); + } + }); + + it("uses the credential when no default auth type is configured", () => { + expect(resolveProxmoxImportAuth(undefined, 42)).toEqual({ + authType: "credential", + credentialId: 42, + overrideCredentialUsername: 1, + }); + expect(resolveProxmoxImportAuth(undefined, null)).toEqual({ + authType: "none", + credentialId: null, + overrideCredentialUsername: 0, + }); + }); + + it("keeps secretless auth types, with or without a credential", () => { + for (const authType of ["none", "agent", "opkssh", "tailscale", "vault"]) { + for (const credentialId of [null, 7]) { + expect(resolveProxmoxImportAuth(authType, credentialId)).toEqual({ + authType, + credentialId: null, + overrideCredentialUsername: 0, + }); + } + } + }); +}); diff --git a/src/backend/tests/database/routes/rbac-host-auth-override.test.ts b/src/backend/tests/database/routes/rbac-host-auth-override.test.ts new file mode 100644 index 0000000..232bf0d --- /dev/null +++ b/src/backend/tests/database/routes/rbac-host-auth-override.test.ts @@ -0,0 +1,274 @@ +import express from "express"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + authenticated: true, + access: { + hasAccess: true, + isShared: true, + isAdminBypass: false, + }, + credentialOwned: true, + credentialId: 7 as number | null, + writes: [] as Array<{ protocol: string; credentialId: number | null }>, + auditCalls: [] as Array>, +})); + +vi.mock("../../../utils/auth-manager.js", () => ({ + AuthManager: { + getInstance: () => ({ + createAuthMiddleware: + () => + ( + req: express.Request & { userId?: string }, + res: express.Response, + next: express.NextFunction, + ) => { + if (!state.authenticated) { + res.status(401).json({ error: "Not authenticated" }); + return; + } + req.userId = "recipient"; + next(); + }, + createDataAccessMiddleware: + () => + ( + _req: express.Request, + _res: express.Response, + next: express.NextFunction, + ) => + next(), + }), + }, +})); + +vi.mock("../../../utils/permission-manager.js", () => ({ + SHARE_PERMISSION_LEVELS: ["connect", "view", "edit", "manage"], + PermissionManager: { + getInstance: () => ({ + canAccessHost: async () => state.access, + requireAdmin: + () => + ( + _req: express.Request, + _res: express.Response, + next: express.NextFunction, + ) => + next(), + invalidateUserPermissionCache: vi.fn(), + isAdmin: async () => false, + }), + }, +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentCredentialRepository: () => ({ + findByIdForUser: async () => + state.credentialOwned ? { id: state.credentialId } : null, + }), + createCurrentSharedHostAuthOverrideRepository: () => ({ + findCredentialId: async () => state.credentialId, + setCredential: async ( + _hostId: number, + _userId: string, + protocol: string, + id: number, + ) => { + state.credentialId = id; + state.writes.push({ protocol, credentialId: id }); + }, + clearCredential: async ( + _hostId: number, + _userId: string, + protocol: string, + ) => { + state.credentialId = null; + state.writes.push({ protocol, credentialId: null }); + return true; + }, + }), + createCurrentUserRepository: () => ({ + findById: async () => ({ id: "recipient", username: "recipient" }), + }), + createCurrentHostFolderRepository: vi.fn(), + createCurrentHostResolutionRepository: vi.fn(), + createCurrentRbacAccessRepository: vi.fn(), + createCurrentRoleRepository: vi.fn(), + createCurrentSnippetRepository: vi.fn(), +})); + +vi.mock("../../../utils/audit-logger.js", () => ({ + getRequestMeta: () => ({ ipAddress: "", userAgent: "" }), + logAudit: vi.fn(async (entry: Record) => { + state.auditCalls.push(entry); + }), +})); + +vi.mock("../../../utils/logger.js", () => ({ + databaseLogger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + success: vi.fn(), + }, +})); + +describe("shared host authentication override routes", () => { + let router: express.Router; + + beforeAll(async () => { + ({ default: router } = await import("../../../database/routes/rbac.js")); + }); + + async function invoke( + method: "get" | "put", + body: Record = {}, + protocol = "ssh", + ): Promise<{ status: number; body: unknown }> { + const routeLayer = ( + router as unknown as { + stack: Array<{ + route?: { + path: string; + methods: Record; + stack: Array<{ + handle: ( + req: express.Request, + res: express.Response, + next: express.NextFunction, + ) => unknown; + }>; + }; + }>; + } + ).stack.find( + (layer) => + layer.route?.path === "/host-access/:hostId/auth/:protocol" && + layer.route.methods[method], + ); + if (!routeLayer?.route) throw new Error(`Missing ${method} route`); + + const handlers = routeLayer.route.stack.map((layer) => layer.handle); + const req = { + params: { hostId: "42", protocol }, + body, + headers: {}, + ip: "127.0.0.1", + } as unknown as express.Request; + + return new Promise((resolve, reject) => { + let index = 0; + let status = 200; + const res = { + status(code: number) { + status = code; + return this; + }, + json(responseBody: unknown) { + resolve({ status, body: responseBody }); + return this; + }, + } as unknown as express.Response; + + const next: express.NextFunction = (error?: unknown) => { + if (error) { + reject(error); + return; + } + const handler = handlers[index++]; + if (!handler) { + resolve({ status, body: undefined }); + return; + } + try { + Promise.resolve(handler(req, res, next)).catch(reject); + } catch (handlerError) { + reject(handlerError); + } + }; + next(); + }); + } + + beforeEach(() => { + state.authenticated = true; + state.access = { + hasAccess: true, + isShared: true, + isAdminBypass: false, + }; + state.credentialOwned = true; + state.credentialId = 7; + state.writes = []; + state.auditCalls = []; + }); + + it("returns the current override for a role-derived shared recipient", async () => { + const response = await invoke("get"); + expect(response).toEqual({ + status: 200, + body: { protocol: "ssh", credentialId: 7 }, + }); + }); + + it("sets and clears a direct recipient's own credential", async () => { + const setResponse = await invoke("put", { credentialId: 8 }); + expect(setResponse.status).toBe(200); + expect(state.writes).toEqual([{ protocol: "ssh", credentialId: 8 }]); + + const clearResponse = await invoke("put", { credentialId: null }); + expect(clearResponse.status).toBe(200); + expect(state.writes).toEqual([ + { protocol: "ssh", credentialId: 8 }, + { protocol: "ssh", credentialId: null }, + ]); + expect(state.auditCalls).toHaveLength(2); + expect(JSON.parse(String(state.auditCalls[0].details))).toEqual({ + protocol: "ssh", + credentialId: 8, + }); + }); + + it("rejects owners, admin bypasses, and users without active access", async () => { + for (const access of [ + { hasAccess: true, isShared: false, isAdminBypass: false }, + { hasAccess: true, isShared: false, isAdminBypass: true }, + { hasAccess: false, isShared: true, isAdminBypass: false }, + ]) { + state.access = access; + const response = await invoke("get"); + expect(response.status).toBe(403); + } + }); + + it("rejects invalid or foreign credentials and unauthenticated requests", async () => { + const invalidResponse = await invoke("put", { credentialId: 0 }); + expect(invalidResponse.status).toBe(400); + + state.credentialOwned = false; + const foreignResponse = await invoke("put", { credentialId: 99 }); + expect(foreignResponse.status).toBe(404); + + state.authenticated = false; + const unauthenticatedResponse = await invoke("get"); + expect(unauthenticatedResponse.status).toBe(401); + }); + + it("rejects recognized but unsupported protocols and invalid protocol names", async () => { + const unsupportedResponse = await invoke("get", {}, "rdp"); + expect(unsupportedResponse).toEqual({ + status: 400, + body: { + error: "RDP authentication overrides are not supported yet", + }, + }); + expect(state.writes).toEqual([]); + + const invalidResponse = await invoke("get", {}, "smtp"); + expect(invalidResponse).toEqual({ + status: 400, + body: { error: "Invalid authentication protocol" }, + }); + }); +}); diff --git a/src/backend/tests/database/routes/session-log-routes.test.ts b/src/backend/tests/database/routes/session-log-routes.test.ts index 1bbbbf5..73dfe88 100644 --- a/src/backend/tests/database/routes/session-log-routes.test.ts +++ b/src/backend/tests/database/routes/session-log-routes.test.ts @@ -28,6 +28,25 @@ vi.mock("../../../utils/auth-manager.js", () => ({ }, })); +// The route module calls PermissionManager.getInstance() at import time and +// pulls in the repository factory, which loads the drizzle schema and the +// better-sqlite3 native binding. Importing that tree costs seconds under a +// concurrent full run โ€” enough to blow the 5s test timeout โ€” and none of it is +// under test here. +vi.mock("../../../utils/permission-manager.js", () => ({ + PermissionManager: { + getInstance: () => ({ + canAccessHost: vi.fn(), + }), + }, +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentSessionRecordingRepository: vi.fn(), + createCurrentSettingsRepository: vi.fn(), + getCurrentSettingValue: vi.fn(), +})); + const mockReadFile = vi.fn(); const mockStat = vi.fn(); const mockUnlink = vi.fn(); diff --git a/src/backend/tests/database/routes/snippets-execution.test.ts b/src/backend/tests/database/routes/snippets-execution.test.ts new file mode 100644 index 0000000..de4306a --- /dev/null +++ b/src/backend/tests/database/routes/snippets-execution.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { + createSnippetExecutionResult, + getSnippetExecutionTimeoutMs, + resolveSnippetCommand, +} from "../../../database/routes/snippets-execution.js"; + +describe("snippet execution", () => { + it("treats stderr as diagnostic output when the command succeeds", () => { + expect(createSnippetExecutionResult(0, "done\n", "warning\n")).toEqual({ + success: true, + output: "done\n", + error: "warning\n", + }); + }); + + it("uses the exit code to report command failure", () => { + expect(createSnippetExecutionResult(1, "", "failed\n")).toEqual({ + success: false, + output: "", + error: "failed\n", + }); + }); + + it("preserves the previous fallback when no exit code is available", () => { + expect(createSnippetExecutionResult(null, "done\n", "")).toEqual({ + success: true, + output: "done\n", + }); + expect(createSnippetExecutionResult(null, "", "failed\n").success).toBe( + false, + ); + }); + + it("disables the command timeout by default", () => { + expect(getSnippetExecutionTimeoutMs(undefined)).toBeUndefined(); + }); + + it("converts a configured timeout from seconds to milliseconds", () => { + expect(getSnippetExecutionTimeoutMs("45")).toBe(45_000); + }); + + it.each(["", "0", "-1", "invalid"])( + "ignores invalid timeout value %j", + (value) => { + expect(getSnippetExecutionTimeoutMs(value)).toBeUndefined(); + }, + ); +}); + +describe("resolveSnippetCommand", () => { + const host = { ip: "10.0.0.5", username: "root", port: 22, name: "web-01" }; + + it("substitutes host variables per target host", () => { + expect( + resolveSnippetCommand("ssh $USER@$HOST -p $PORT # $NAME", host), + ).toBe("ssh root@10.0.0.5 -p 22 # web-01"); + }); + + it("supports brace syntax for host variables", () => { + expect(resolveSnippetCommand("ping ${HOST}", host)).toBe("ping 10.0.0.5"); + }); + + it("substitutes input placeholders from inputValues", () => { + expect( + resolveSnippetCommand("nc -zv $HOST ${INPUT_1:Port}", host, { + INPUT_1: "8080", + }), + ).toBe("nc -zv 10.0.0.5 8080"); + }); + + it("leaves host variables literal when no host context is given", () => { + expect(resolveSnippetCommand("ping $HOST", null)).toBe("ping $HOST"); + }); + + it("leaves input placeholders literal when no value was supplied", () => { + expect(resolveSnippetCommand("echo $INPUT_1", null)).toBe("echo $INPUT_1"); + }); +}); diff --git a/src/backend/tests/database/routes/sync-locate-row.test.ts b/src/backend/tests/database/routes/sync-locate-row.test.ts new file mode 100644 index 0000000..1d4972a --- /dev/null +++ b/src/backend/tests/database/routes/sync-locate-row.test.ts @@ -0,0 +1,83 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { and, eq } from "drizzle-orm"; +import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core"; +import { describe, expect, it } from "vitest"; +import { locateSyncRow } from "../../../database/routes/sync.js"; +import { hosts, userPreferences } from "../../../database/db/schema.js"; + +/** + * A sync push locates the stored row twice: once to see whether it exists, + * once to write it. Those lookups used to be spelled out separately, and only + * the read knew about singleton entities โ€” the write always keyed on + * `table.id`. + * + * `user_preferences` is the only singleton, and the one synced table whose + * primary key is `user_id` with no `id` column at all. `table.id` was + * therefore `undefined` and drizzle emitted a comparison with nothing on its + * left: `( = ? and "user_preferences"."user_id" = ?)`. The insert branch was + * fine, so the first push of preferences succeeded and every push after it โ€” + * the steady state โ€” failed with `SqliteError: near "=": syntax error`. + */ +describe("locateSyncRow", () => { + const dialect = new SQLiteSyncDialect(); + + const toSql = (condition: Parameters[0]): string => + dialect.sqlToQuery(condition).sql; + + /** `= ?` with no operand to its left โ€” what used to reach SQLite. */ + const EMPTY_LEFT_OPERAND = /(^|\(|\band\b|\bor\b)\s*=\s*\?/; + + it("keys a singleton entity on its owner", () => { + const sql = toSql(locateSyncRow("userPreferences", "user-1", "ignored")); + + expect(sql).toContain('"user_id"'); + expect(sql).not.toContain('"sync_id"'); + expect(sql).not.toMatch(EMPTY_LEFT_OPERAND); + }); + + it("keys a regular entity on its sync id and owner", () => { + const sql = toSql(locateSyncRow("hosts", "user-1", "sync-abc")); + + expect(sql).toContain('"sync_id"'); + expect(sql).toContain('"user_id"'); + expect(sql).not.toMatch(EMPTY_LEFT_OPERAND); + }); + + it("is the shape the id-keyed lookup could not produce", () => { + // Guards the assertions above: the pattern really does catch the old SQL. + const previous = and( + eq((userPreferences as unknown as typeof hosts).id, 1), + eq(userPreferences.userId, "user-1"), + )!; + + expect(toSql(previous)).toMatch(EMPTY_LEFT_OPERAND); + }); + + it("updates a preferences row that already exists", () => { + const sqlite = new Database(":memory:"); + sqlite.exec(` + CREATE TABLE user_preferences ( + user_id TEXT PRIMARY KEY, + theme TEXT + ); + `); + const db = drizzle(sqlite, { schema: { userPreferences } }); + + // The steady state: a row exists, so the push takes the update path. + sqlite + .prepare("INSERT INTO user_preferences (user_id, theme) VALUES (?, ?)") + .run("user-1", "dark"); + + const updated = db + .update(userPreferences) + .set({ theme: "light" }) + .where(locateSyncRow("userPreferences", "user-1", "ignored")) + .returning({ theme: userPreferences.theme }) + .all(); + + expect(updated).toEqual([{ theme: "light" }]); + + sqlite.close(); + }); +}); diff --git a/src/backend/tests/database/routes/sync.test.ts b/src/backend/tests/database/routes/sync.test.ts new file mode 100644 index 0000000..dc8cd67 --- /dev/null +++ b/src/backend/tests/database/routes/sync.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from "vitest"; +import syncRouter, { + isValidEntityType, + stripWritePayload, +} from "../../../database/routes/sync.js"; + +describe("sync route order", () => { + it("registers POST /tombstones before the POST /:entityType wildcard", () => { + const postPaths = ( + syncRouter as unknown as { + stack: Array<{ route?: { path: string; methods: { post?: boolean } } }>; + } + ).stack + .filter((layer) => layer.route?.methods?.post) + .map((layer) => layer.route!.path); + + // "/tombstones" is a valid value for :entityType as far as Express is + // concerned, so registering the wildcard first makes the tombstone + // endpoint unreachable -- every deletion push answers 400 "Unknown entity + // type" instead of applying the deletion. + expect(postPaths).toContain("/tombstones"); + expect(postPaths.indexOf("/tombstones")).toBeLessThan( + postPaths.indexOf("/:entityType"), + ); + }); +}); + +describe("isValidEntityType", () => { + it("accepts every whitelisted sync entity type", () => { + for (const type of [ + "hosts", + "sshCredentials", + "sshFolders", + "snippets", + "snippetFolders", + "vaultProfiles", + "dashboardServiceLinks", + "homepageItems", + "userPreferences", + ]) { + expect(isValidEntityType(type)).toBe(true); + } + }); + + it("rejects unknown or non-string entity types", () => { + expect(isValidEntityType("hostAccess")).toBe(false); + expect(isValidEntityType("")).toBe(false); + expect(isValidEntityType(undefined)).toBe(false); + expect(isValidEntityType(42)).toBe(false); + }); +}); + +describe("stripWritePayload", () => { + it("strips id, userId, and syncId from every entity type", () => { + const payload = { + id: 1, + userId: "user-1", + syncId: "abc", + name: "prod-db", + }; + expect(stripWritePayload("sshFolders", payload)).toEqual({ + name: "prod-db", + }); + }); + + it("also strips desktop-only fields flagged read-only for hosts", () => { + const payload = { + id: 1, + userId: "user-1", + syncId: "abc", + name: "web", + connectionOrigin: "remote", + }; + expect(stripWritePayload("hosts", payload)).toEqual({ name: "web" }); + }); + + it("keeps preference storage mode local to each device", () => { + expect( + stripWritePayload("userPreferences", { + syncId: "userPreferences:singleton", + theme: "dark", + storageMode: "cloud", + }), + ).toEqual({ theme: "dark" }); + }); + + it("does not mutate the original payload object", () => { + const payload = { id: 1, userId: "user-1", syncId: "abc", name: "x" }; + stripWritePayload("snippets", payload); + expect(payload).toEqual({ + id: 1, + userId: "user-1", + syncId: "abc", + name: "x", + }); + }); +}); diff --git a/src/backend/tests/database/routes/terminal-image-storage-settings-route.test.ts b/src/backend/tests/database/routes/terminal-image-storage-settings-route.test.ts new file mode 100644 index 0000000..97c779c --- /dev/null +++ b/src/backend/tests/database/routes/terminal-image-storage-settings-route.test.ts @@ -0,0 +1,304 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { EventEmitter } from "events"; +import fs from "fs/promises"; +import os from "os"; +import path from "path"; +import express, { + type Request, + type RequestHandler, + type Response, +} from "express"; + +const state = vi.hoisted(() => ({ + userId: "admin-1", + settings: {} as Record, + sessions: [] as unknown[], +})); + +vi.mock("../../../utils/logger.js", () => ({ + authLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, + databaseLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, +})); + +vi.mock("../../../hosts/terminal/session-manager.js", () => ({ + sessionManager: { + getUserSessions: () => state.sessions, + }, +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentSettingsRepository: () => ({ + get: async (key: string) => state.settings[key] ?? null, + set: async (key: string, value: string) => { + state.settings[key] = value; + }, + setMany: async (writes: Array<{ key: string; value: string }>) => { + for (const write of writes) state.settings[write.key] = write.value; + }, + }), +})); + +const { registerUserImageStorageRoutes } = + await import("../../../database/routes/user-image-storage-routes.js"); + +const requireAdmin: RequestHandler = (_req, _res, next) => next(); +const router = express.Router(); +registerUserImageStorageRoutes(router, requireAdmin); + +interface RouteLayer { + route?: { + path: string; + methods: Record; + stack: Array<{ handle: RequestHandler }>; + }; +} + +function handlerFor(pathName: string, method: string): RequestHandler { + const layer = (router as unknown as { stack: RouteLayer[] }).stack.find( + (candidate) => + candidate.route?.path === pathName && candidate.route.methods[method], + ); + if (!layer) throw new Error(`Route not registered: ${method} ${pathName}`); + return layer.route!.stack[layer.route!.stack.length - 1]!.handle; +} + +const getHandler = handlerFor("/terminal-image-storage-settings", "get"); +const patchHandler = handlerFor("/terminal-image-storage-settings", "patch"); +const testHandler = handlerFor("/terminal-image-storage-settings/test", "post"); + +async function invoke(handler: RequestHandler, body?: unknown) { + const req = { + userId: state.userId, + body: body ?? {}, + headers: {}, + } as unknown as Request; + const result = { statusCode: 200, body: null as unknown }; + const res = { + status(code: number) { + result.statusCode = code; + return this; + }, + json(payload: unknown) { + result.body = payload; + return this; + }, + } as unknown as Response; + await handler(req, res, () => {}); + return result; +} + +/** Fake ssh2 exec channel: every command exits 0. */ +function fakeSshConn() { + return { + exec( + _command: string, + callback: (error: Error | undefined, stream?: unknown) => void, + ) { + const stream = new EventEmitter() as EventEmitter & { + resume: () => void; + }; + stream.resume = () => queueMicrotask(() => stream.emit("close", 0)); + callback(undefined, stream); + }, + }; +} + +const LEGACY_ENV_NAMES = [ + "TERMIX_IMAGE_STORAGE_MODE", + "TERMIX_IMAGE_DIR", + "TERMIX_IMAGE_HOST_PATH", + "TERMIX_IMAGE_TTL_MS", + "TERMIX_MAX_IMAGE_COUNT", + "TERMIX_MAX_IMAGE_STORAGE_BYTES", + "DATA_DIR", +]; + +let savedEnv: Record; +let localDir: string; + +beforeEach(async () => { + state.settings = {}; + state.sessions = []; + savedEnv = Object.fromEntries( + LEGACY_ENV_NAMES.map((name) => [name, process.env[name]]), + ); + for (const name of LEGACY_ENV_NAMES) delete process.env[name]; + // The settings validator rejects backslashes, so use a POSIX-style form of + // the real temp dir. Windows still resolves it, so file checks keep working. + localDir = ( + await fs.mkdtemp(path.join(os.tmpdir(), "termix-image-test-")) + ).replace(/\\/g, "/"); +}); + +afterEach(async () => { + for (const name of LEGACY_ENV_NAMES) { + if (savedEnv[name] === undefined) delete process.env[name]; + else process.env[name] = savedEnv[name]; + } + await fs.rm(localDir, { recursive: true, force: true }); +}); + +describe("GET /users/terminal-image-storage-settings", () => { + it("returns the public settings shape without the backend localDir", async () => { + state.settings["terminal_image_storage_mode"] = "local"; + state.settings["terminal_image_local_dir"] = localDir; + state.settings["terminal_image_host_path"] = "/mnt/images"; + + const response = await invoke(getHandler); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + mode: "local", + hostPath: "/mnt/images", + ttlMs: 3_600_000, + maxCount: 100, + maxBytes: 5_368_709_120, + localMappingConfigured: true, + }); + expect(JSON.stringify(response.body)).not.toContain(localDir); + }); +}); + +describe("PATCH /users/terminal-image-storage-settings", () => { + it("rejects an invalid mode with a safe 400", async () => { + const response = await invoke(patchHandler, { mode: "nfs" }); + expect(response.statusCode).toBe(400); + expect(response.body).toEqual({ + error: "Invalid value for mode", + code: "IMAGE_STORAGE_SETTINGS_INVALID", + field: "mode", + }); + }); + + it("rejects a relative localDir", async () => { + const response = await invoke(patchHandler, { localDir: "images/tmp" }); + expect(response.statusCode).toBe(400); + expect(response.body).toMatchObject({ + code: "IMAGE_STORAGE_SETTINGS_INVALID", + field: "localDir", + }); + expect(JSON.stringify(response.body)).not.toContain("images/tmp"); + }); + + it("rejects out-of-range numeric limits", async () => { + const response = await invoke(patchHandler, { maxBytes: 1024 }); + expect(response.statusCode).toBe(400); + expect(response.body).toMatchObject({ + code: "IMAGE_STORAGE_SETTINGS_INVALID", + field: "maxBytes", + }); + }); + + it("rejects unknown fields without writing anything", async () => { + const response = await invoke(patchHandler, { shellPath: "/tmp/x" }); + expect(response.statusCode).toBe(400); + expect(response.body).toMatchObject({ + code: "IMAGE_STORAGE_SETTINGS_UNKNOWN_FIELD", + }); + expect(state.settings).toEqual({}); + }); + + // On Windows path.resolve turns the stored dir back into a backslash path, + // which the validator rejects on read, so the round trip only holds on POSIX. + it.skipIf(process.platform === "win32")( + "persists a valid partial update and returns the public shape", + async () => { + const response = await invoke(patchHandler, { + mode: "local", + localDir, + hostPath: "/host/images", + ttlMs: 60_000, + maxCount: 5, + maxBytes: 10_485_760, + }); + + expect(response.statusCode).toBe(200); + expect(state.settings).toEqual({ + terminal_image_storage_mode: "local", + terminal_image_local_dir: path.resolve(localDir), + terminal_image_host_path: "/host/images", + terminal_image_ttl_ms: "60000", + terminal_image_max_count: "5", + terminal_image_max_storage_bytes: "10485760", + }); + expect(response.body).toMatchObject({ + mode: "local", + hostPath: "/host/images", + ttlMs: 60_000, + maxCount: 5, + maxBytes: 10_485_760, + localMappingConfigured: true, + }); + expect(JSON.stringify(response.body)).not.toContain(localDir); + }, + ); +}); + +describe("POST /users/terminal-image-storage-settings/test", () => { + it("requires an instanceId", async () => { + const response = await invoke(testHandler, {}); + expect(response.statusCode).toBe(400); + expect(response.body).toMatchObject({ code: "IMAGE_SESSION_MISSING" }); + }); + + it("reports unavailable storage when no session is connected", async () => { + const response = await invoke(testHandler, { instanceId: "tab-1" }); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + mode: "auto", + connected: false, + remoteSftpAvailable: false, + localHostVisible: null, + selectedMode: "unavailable", + localMappingConfigured: false, + }); + }); + + it("probes local visibility through the connected session only", async () => { + state.settings["terminal_image_local_dir"] = localDir; + state.settings["terminal_image_host_path"] = "/host/images"; + state.sessions = [ + { + tabInstanceId: "tab-1", + isConnected: true, + sshConn: fakeSshConn(), + }, + ]; + + const response = await invoke(testHandler, { instanceId: "tab-1" }); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + mode: "auto", + connected: true, + remoteSftpAvailable: true, + localHostVisible: true, + selectedMode: "local", + localMappingConfigured: true, + }); + // The bounded probe cleans up after itself. + expect( + (await fs.readdir(localDir)).filter((f) => f.includes("probe")), + ).toEqual([]); + }); + + it("does not probe sessions owned by other instance IDs", async () => { + state.sessions = [ + { + tabInstanceId: "tab-2", + isConnected: true, + sshConn: fakeSshConn(), + }, + ]; + + const response = await invoke(testHandler, { instanceId: "tab-1" }); + + expect(response.statusCode).toBe(200); + expect(response.body).toMatchObject({ + connected: false, + remoteSftpAvailable: false, + localHostVisible: null, + selectedMode: "unavailable", + }); + }); +}); diff --git a/src/backend/tests/database/routes/terminal-image-storage-settings.test.ts b/src/backend/tests/database/routes/terminal-image-storage-settings.test.ts new file mode 100644 index 0000000..f2195c1 --- /dev/null +++ b/src/backend/tests/database/routes/terminal-image-storage-settings.test.ts @@ -0,0 +1,192 @@ +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { + DEFAULT_IMAGE_HOST_PATH, + DEFAULT_IMAGE_MAX_BYTES, + DEFAULT_IMAGE_MAX_COUNT, + DEFAULT_IMAGE_TTL_MS, + defaultImageLocalDir, + parseImageHostPath, + parseImageLocalDir, + parseTerminalImageStorageMode, + resolveTerminalImageStorageSettings, + TERMINAL_IMAGE_STORAGE_KEYS, +} from "../../../database/routes/terminal-image-storage-settings.js"; +import { SettingsRepository } from "../../../database/repositories/settings-repository.js"; +import { TestSqliteDatabase } from "../repositories/test-support.js"; + +// parseImageLocalDir resolves against the host platform, so a POSIX literal +// becomes a drive-rooted path on Windows. Compare against the same resolution. +function localDir(posixPath: string): string { + return path.resolve(posixPath); +} + +function stubSettings(values: Record = {}) { + return { + get: async (key: string) => values[key] ?? null, + }; +} + +const EMPTY_ENV: NodeJS.ProcessEnv = {}; + +describe("terminal image storage settings", () => { + describe("mode parsing", () => { + it("accepts the three documented modes case-insensitively", () => { + expect(parseTerminalImageStorageMode("auto")).toBe("auto"); + expect(parseTerminalImageStorageMode("LOCAL")).toBe("local"); + expect(parseTerminalImageStorageMode(" remote-sftp ")).toBe( + "remote-sftp", + ); + }); + + it("rejects unknown modes and non-strings", () => { + expect(parseTerminalImageStorageMode("s3")).toBeNull(); + expect(parseTerminalImageStorageMode("")).toBeNull(); + expect(parseTerminalImageStorageMode(undefined)).toBeNull(); + }); + }); + + describe("path validation", () => { + it("accepts absolute local directories and normalizes them", () => { + expect(parseImageLocalDir("/var/lib/termix/images")).toBe( + localDir("/var/lib/termix/images"), + ); + expect(parseImageLocalDir("/var/lib/termix/../termix/images")).toBeNull(); + }); + + it("rejects relative, empty and NUL-containing local directories", () => { + expect(parseImageLocalDir("images")).toBeNull(); + expect(parseImageLocalDir("./db/data/images")).toBeNull(); + expect(parseImageLocalDir("")).toBeNull(); + expect(parseImageLocalDir("/tmp/a\0b")).toBeNull(); + }); + + it("requires the agent-visible host path to be POSIX-absolute", () => { + expect(parseImageHostPath("/tmp/termix-image-v0")).toBe( + "/tmp/termix-image-v0", + ); + expect(parseImageHostPath("tmp/termix-image-v0")).toBeNull(); + expect(parseImageHostPath("C:\\images")).toBeNull(); + expect(parseImageHostPath("/tmp/a\0b")).toBeNull(); + }); + }); + + describe("resolution precedence", () => { + it("uses built-in defaults when neither the database nor env has values", async () => { + const resolved = await resolveTerminalImageStorageSettings( + stubSettings(), + EMPTY_ENV, + ); + expect(resolved.mode).toBe("auto"); + expect(resolved.localDir).toBe(defaultImageLocalDir(EMPTY_ENV)); + expect(resolved.hostPath).toBe(DEFAULT_IMAGE_HOST_PATH); + expect(resolved.ttlMs).toBe(DEFAULT_IMAGE_TTL_MS); + expect(resolved.maxCount).toBe(DEFAULT_IMAGE_MAX_COUNT); + expect(resolved.maxBytes).toBe(DEFAULT_IMAGE_MAX_BYTES); + }); + + it("seeds defaults from legacy TERMIX_IMAGE_* env when no DB value exists", async () => { + const resolved = await resolveTerminalImageStorageSettings( + stubSettings(), + { + TERMIX_IMAGE_DIR: "/host-tmp/images", + TERMIX_IMAGE_HOST_PATH: "/tmp/images", + TERMIX_IMAGE_TTL_MS: "60000", + TERMIX_MAX_IMAGE_COUNT: "5", + TERMIX_MAX_IMAGE_STORAGE_BYTES: "10485760", + }, + ); + expect(resolved.localDir).toBe(localDir("/host-tmp/images")); + expect(resolved.hostPath).toBe("/tmp/images"); + expect(resolved.ttlMs).toBe(60_000); + expect(resolved.maxCount).toBe(5); + expect(resolved.maxBytes).toBe(10_485_760); + }); + + it("lets persisted DB values win over legacy env values", async () => { + const resolved = await resolveTerminalImageStorageSettings( + stubSettings({ + [TERMINAL_IMAGE_STORAGE_KEYS.localDir]: "/db/images", + [TERMINAL_IMAGE_STORAGE_KEYS.ttlMs]: "1000", + [TERMINAL_IMAGE_STORAGE_KEYS.maxCount]: "7", + }), + { + TERMIX_IMAGE_DIR: "/host-tmp/images", + TERMIX_IMAGE_TTL_MS: "60000", + TERMIX_MAX_IMAGE_COUNT: "5", + }, + ); + expect(resolved.localDir).toBe(localDir("/db/images")); + expect(resolved.ttlMs).toBe(1_000); + expect(resolved.maxCount).toBe(7); + }); + + it("falls through to env and defaults when the DB value is invalid", async () => { + const resolved = await resolveTerminalImageStorageSettings( + stubSettings({ + [TERMINAL_IMAGE_STORAGE_KEYS.localDir]: "relative/path", + [TERMINAL_IMAGE_STORAGE_KEYS.ttlMs]: "not-a-number", + }), + { TERMIX_IMAGE_DIR: "/host-tmp/images" }, + ); + expect(resolved.localDir).toBe(localDir("/host-tmp/images")); + expect(resolved.ttlMs).toBe(DEFAULT_IMAGE_TTL_MS); + }); + + it("keeps legacy explicit local mappings on local mode", async () => { + const resolved = await resolveTerminalImageStorageSettings( + stubSettings(), + { TERMIX_IMAGE_DIR: "/host-tmp/images" }, + ); + expect(resolved.mode).toBe("local"); + // hostPath falls back to the legacy local dir, which resolves natively. + expect(resolved.hostPath).toBe(localDir("/host-tmp/images")); + expect(resolved.localMappingConfigured).toBe(true); + }); + + it("lets a persisted DB mode override the legacy local mapping", async () => { + const resolved = await resolveTerminalImageStorageSettings( + stubSettings({ + [TERMINAL_IMAGE_STORAGE_KEYS.mode]: "remote-sftp", + }), + { TERMIX_IMAGE_DIR: "/host-tmp/images" }, + ); + expect(resolved.mode).toBe("remote-sftp"); + }); + + it("clamps out-of-range numeric values like the legacy env parsing did", async () => { + const resolved = await resolveTerminalImageStorageSettings( + stubSettings(), + { + TERMIX_IMAGE_TTL_MS: "-5", + TERMIX_MAX_IMAGE_COUNT: "0", + TERMIX_MAX_IMAGE_STORAGE_BYTES: "10", + }, + ); + expect(resolved.ttlMs).toBe(0); + expect(resolved.maxCount).toBe(1); + expect(resolved.maxBytes).toBe(1_048_576); + }); + + it("reads persisted values through the real settings repository", async () => { + const adapter = new TestSqliteDatabase(); + try { + const context = await adapter.connect(); + const repository = new SettingsRepository(context); + await repository.set(TERMINAL_IMAGE_STORAGE_KEYS.mode, "local"); + await repository.set( + TERMINAL_IMAGE_STORAGE_KEYS.localDir, + "/persisted/images", + ); + + const resolved = await resolveTerminalImageStorageSettings(repository, { + TERMIX_IMAGE_DIR: "/host-tmp/images", + }); + expect(resolved.mode).toBe("local"); + expect(resolved.localDir).toBe(localDir("/persisted/images")); + } finally { + await adapter.close(); + } + }); + }); +}); diff --git a/src/backend/tests/database/routes/terminal-image-storage.test.ts b/src/backend/tests/database/routes/terminal-image-storage.test.ts new file mode 100644 index 0000000..2f15a53 --- /dev/null +++ b/src/backend/tests/database/routes/terminal-image-storage.test.ts @@ -0,0 +1,571 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { EventEmitter } from "events"; +import fs from "fs/promises"; +import os from "os"; +import path from "path"; +import { randomUUID } from "crypto"; +import { + REMOTE_IMAGE_DIR, + selectImageStorageMode, + storeImageLocally, + storeImageViaSftp, + TerminalImageStorageError, + type ImageSftpClient, +} from "../../../database/routes/terminal-image-storage.js"; +import type { TerminalImageStorageSettings } from "../../../database/routes/terminal-image-storage-settings.js"; + +const PNG_BYTES = Buffer.from([0x89, 0x50, 0x4e, 0x47]); + +function settings( + overrides: Partial, +): TerminalImageStorageSettings { + return { + mode: "local", + localDir: "/nonexistent", + hostPath: "/tmp/termix-image-v0", + ttlMs: 3_600_000, + maxCount: 100, + maxBytes: 5_368_709_120, + localMappingConfigured: false, + ...overrides, + }; +} + +describe("selectImageStorageMode", () => { + it("keeps explicit modes deterministic regardless of capability", () => { + expect( + selectImageStorageMode(settings({ mode: "local" }), { + remoteSftpAvailable: true, + }), + ).toBe("local"); + expect( + selectImageStorageMode(settings({ mode: "remote-sftp" }), { + remoteSftpAvailable: false, + }), + ).toBe("remote-sftp"); + }); + + it("falls back on capability only in auto mode", () => { + expect( + selectImageStorageMode(settings({ mode: "auto" }), { + remoteSftpAvailable: true, + }), + ).toBe("remote-sftp"); + expect( + selectImageStorageMode(settings({ mode: "auto" }), { + remoteSftpAvailable: false, + }), + ).toBe("unavailable"); + expect( + selectImageStorageMode( + settings({ mode: "auto", localMappingConfigured: true }), + { remoteSftpAvailable: false, localHostVisible: true }, + ), + ).toBe("local"); + }); +}); + +describe("storeImageLocally", () => { + let dir: string; + + beforeEach(async () => { + dir = await fs.mkdtemp(path.join(os.tmpdir(), "termix-images-test-")); + }); + + afterEach(async () => { + await fs.rm(dir, { recursive: true, force: true }); + }); + + async function seedFile(bytes: number, mtimeMs?: number): Promise { + const name = `${randomUUID()}.png`; + const filePath = path.join(dir, name); + await fs.writeFile(filePath, Buffer.alloc(bytes)); + if (mtimeMs !== undefined) { + const date = new Date(mtimeMs); + await fs.utimes(filePath, date, date); + } + return name; + } + + it("writes a UUID-named PNG and returns the agent-visible host path", async () => { + const stored = await storeImageLocally( + PNG_BYTES, + settings({ localDir: dir, hostPath: "/host-view/images" }), + ); + + expect(stored.storage).toBe("local"); + expect(stored.filename).toBe(`${stored.id}.png`); + expect(stored.shellPath).toBe( + path.posix.join("/host-view/images", stored.filename), + ); + expect(stored.shellPath).not.toContain(dir); + await expect(fs.readFile(path.join(dir, stored.filename))).resolves.toEqual( + PNG_BYTES, + ); + }); + + it("rejects with IMAGE_STORAGE_LIMIT_REACHED when the count cap is full", async () => { + await seedFile(10); + const error = await storeImageLocally( + PNG_BYTES, + settings({ localDir: dir, maxCount: 1 }), + ).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_STORAGE_LIMIT_REACHED", + ); + }); + + it("rejects with IMAGE_STORAGE_LIMIT_REACHED when the byte cap is full", async () => { + await seedFile(900); + const error = await storeImageLocally( + Buffer.alloc(200), + settings({ localDir: dir, maxBytes: 1_000 }), + ).catch((caught: unknown) => caught); + + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_STORAGE_LIMIT_REACHED", + ); + }); + + it("cleans expired files during upload so they no longer count", async () => { + await seedFile(10, Date.now() - 2 * 3_600_000); + const stored = await storeImageLocally( + PNG_BYTES, + settings({ localDir: dir, maxCount: 1, ttlMs: 3_600_000 }), + ); + + expect(stored.storage).toBe("local"); + const remaining = await fs.readdir(dir); + expect(remaining).toEqual([stored.filename]); + }); + + it("fails closed when local storage inspection is unavailable", async () => { + const blocked = path.join(dir, "blocked-file"); + await fs.writeFile(blocked, "not a directory"); + + const error = await storeImageLocally( + PNG_BYTES, + settings({ localDir: blocked }), + ).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_LOCAL_INSPECTION_FAILED", + ); + }); + it("reports inspection failures before attempting a write", async () => { + const blocked = path.join(dir, "blocked"); + await fs.writeFile(blocked, "not a directory"); + + const error = await storeImageLocally( + PNG_BYTES, + settings({ localDir: path.join(blocked, "images") }), + ).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_LOCAL_INSPECTION_FAILED", + ); + }); +}); + +describe("storeImageViaSftp", () => { + function fakeSftp(behavior: { + mkdirError?: Error; + writeError?: Error; + stallWrite?: boolean; + stallLock?: boolean; + stallReaddir?: boolean; + stallRmdir?: boolean; + stallUnlink?: boolean; + readdirEntries?: Array<{ filename: string; mtime?: number; size?: number }>; + readdirError?: Error; + statMode?: number; + }): { + sftp: ImageSftpClient; + written: Map; + calls: { + mkdir: Array<{ dir: string; mode?: number }>; + createWriteStream: Array<{ path: string; mode?: number }>; + }; + streams: Array< + NodeJS.WritableStream & { destroy: () => void; destroyed: boolean } + >; + } { + const written = new Map(); + const streams: Array< + NodeJS.WritableStream & { destroy: () => void; destroyed: boolean } + > = []; + const calls = { mkdir: [], createWriteStream: [] } as { + mkdir: Array<{ dir: string; mode?: number }>; + createWriteStream: Array<{ path: string; mode?: number }>; + }; + const sftp = { + mkdir: ( + dir: string, + attrsOrCallback: { mode?: number } | ((err?: Error) => void), + maybeCallback?: (err?: Error) => void, + ) => { + const callback = + typeof attrsOrCallback === "function" + ? attrsOrCallback + : maybeCallback!; + calls.mkdir.push({ + dir, + mode: + typeof attrsOrCallback === "function" + ? undefined + : attrsOrCallback.mode, + }); + if ( + behavior.stallLock && + dir === `${REMOTE_IMAGE_DIR}/.termix-write-lock` + ) { + return; + } + callback(dir === REMOTE_IMAGE_DIR ? behavior.mkdirError : undefined); + }, + stat: ( + _dir: string, + callback: (error: Error | undefined, attrs?: { mode?: number }) => void, + ) => callback(undefined, { mode: behavior.statMode ?? 0o40700 }), + chmod: (_dir: string, _mode: number, callback: (error?: Error) => void) => + callback(), + readdir: ( + _dir: string, + callback: ( + error: Error | undefined, + entries: Array<{ + filename: string; + attrs?: { mtime?: number; size?: number }; + }>, + ) => void, + ) => { + if (behavior.stallReaddir) return; + callback( + behavior.readdirError, + behavior.readdirEntries?.map((entry) => ({ + filename: entry.filename, + attrs: entry, + })) ?? [], + ); + }, + createWriteStream: (remotePath: string, options?: { mode?: number }) => { + calls.createWriteStream.push({ path: remotePath, mode: options?.mode }); + const stream = new EventEmitter() as NodeJS.WritableStream & { + end: (data: Buffer) => void; + destroy: () => void; + destroyed: boolean; + }; + stream.destroyed = false; + stream.destroy = () => { + stream.destroyed = true; + }; + streams.push(stream); + stream.end = (data: Buffer) => { + if (behavior.stallWrite) return; + queueMicrotask(() => { + if (behavior.writeError) { + stream.emit("error", behavior.writeError); + return; + } + written.set(remotePath, data); + stream.emit("close"); + }); + }; + return stream; + }, + unlink: (_remotePath: string, callback: (error?: Error) => void) => { + if (behavior.stallUnlink) return; + callback(); + }, + rmdir: (_dir: string, callback: (error?: Error) => void) => { + if (behavior.stallRmdir) return; + callback(); + }, + } as unknown as ImageSftpClient; + return { sftp, written, calls, streams }; + } + + it("writes into the remote image directory and returns its POSIX path", async () => { + const { sftp, written } = fakeSftp({}); + const stored = await storeImageViaSftp(sftp, PNG_BYTES); + + expect(stored.storage).toBe("remote-sftp"); + expect(stored.shellPath).toBe(`${REMOTE_IMAGE_DIR}/${stored.id}.png`); + expect(written.get(stored.shellPath)).toEqual(PNG_BYTES); + }); + + it("requests restrictive modes for the remote directory and file", async () => { + const { sftp, calls } = fakeSftp({}); + const stored = await storeImageViaSftp(sftp, PNG_BYTES); + + expect(stored.storage).toBe("remote-sftp"); + expect(calls.mkdir).toEqual([ + { dir: REMOTE_IMAGE_DIR, mode: 0o700 }, + { dir: `${REMOTE_IMAGE_DIR}/.termix-write-lock`, mode: 0o700 }, + ]); + expect(calls.createWriteStream).toEqual([ + { path: stored.shellPath, mode: 0o600 }, + ]); + }); + it("removes expired UUID PNGs with best-effort remote retention", async () => { + const nowMs = 10_000_000; + const expiredName = `${randomUUID()}.png`; + const freshName = `${randomUUID()}.png`; + const unlinked: string[] = []; + const base = fakeSftp({}); + const sftp = base.sftp as ImageSftpClient & { + readdir: ( + dir: string, + callback: ( + err: Error | undefined, + entries: Array<{ filename: string; attrs?: { mtime?: number } }>, + ) => void, + ) => void; + unlink: (remotePath: string, callback: (err?: Error) => void) => void; + }; + sftp.readdir = (_dir, callback) => + callback(undefined, [ + { filename: expiredName, attrs: { mtime: (nowMs - 7_200_000) / 1000 } }, + { filename: freshName, attrs: { mtime: nowMs / 1000 } }, + { filename: "other.txt", attrs: { mtime: 0 } }, + ]); + sftp.unlink = (remotePath, callback) => { + unlinked.push(remotePath); + callback(); + }; + + await storeImageViaSftp(sftp, PNG_BYTES, { + ttlMs: 3_600_000, + nowMs, + }); + + expect(unlinked).toEqual([`${REMOTE_IMAGE_DIR}/${expiredName}`]); + }); + it("fails closed when remote quota inspection fails", async () => { + const { sftp } = fakeSftp({ + readdirError: new Error("remote listing failed"), + }); + const error = await storeImageViaSftp(sftp, PNG_BYTES, { + maxCount: 10, + maxBytes: 1024, + }).catch((caught: unknown) => caught); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_QUOTA_UNAVAILABLE", + ); + }); + + it("rejects remote writes at the configured count limit", async () => { + const { sftp } = fakeSftp({ + readdirEntries: [{ filename: `${randomUUID()}.png`, size: 12 }], + }); + const error = await storeImageViaSftp(sftp, PNG_BYTES, { + maxCount: 1, + maxBytes: 1024, + }).catch((caught: unknown) => caught); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_STORAGE_LIMIT_REACHED", + ); + }); + + it("destroys a stalled SFTP write after its timeout", async () => { + const { sftp, streams } = fakeSftp({ stallWrite: true }); + const error = await storeImageViaSftp(sftp, PNG_BYTES, { + writeTimeoutMs: 25, + }).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + expect(streams[0]!.destroyed).toBe(true); + }); + it("rejects an existing remote path that is not a directory", async () => { + const { sftp } = fakeSftp({ + mkdirError: new Error("Failure: file already exists"), + statMode: 0o100644, + }); + const error = await storeImageViaSftp(sftp, PNG_BYTES).catch( + (caught: unknown) => caught, + ); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + }); + + it("preserves SFTP client context for directory inspection", async () => { + const base = fakeSftp({ mkdirError: new Error("already exists") }); + const sftp = base.sftp; + sftp.lstat = function ( + this: ImageSftpClient, + _dir: string, + callback: (error: Error | undefined, attrs?: { mode?: number }) => void, + ) { + if (this !== sftp) { + callback(new Error("SFTP context lost")); + return; + } + callback(undefined, { mode: 0o40700 }); + }; + const result = await storeImageViaSftp(sftp, PNG_BYTES); + expect(result.storage).toBe("remote-sftp"); + }); + + it("tolerates mkdir failures for an already-existing directory", async () => { + const { sftp } = fakeSftp({ + mkdirError: new Error("Failure: file already exists"), + }); + await expect(storeImageViaSftp(sftp, PNG_BYTES)).resolves.toMatchObject({ + storage: "remote-sftp", + }); + }); + + it("cleans up a partially created remote file after a write failure", async () => { + const { sftp } = fakeSftp({ writeError: new Error("write failed") }); + const unlinked: string[] = []; + (sftp as ImageSftpClient).unlink = (remotePath, callback) => { + unlinked.push(remotePath); + callback(); + }; + + const error = await storeImageViaSftp(sftp, PNG_BYTES).catch( + (caught: unknown) => caught, + ); + + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect(unlinked).toHaveLength(1); + expect(unlinked[0]).toMatch( + new RegExp(`${REMOTE_IMAGE_DIR}/[0-9a-f-]+\\.png`), + ); + }); + + it("preserves the write error when partial-file cleanup stalls", async () => { + const { sftp } = fakeSftp({ + writeError: new Error("write failed"), + stallUnlink: true, + }); + const error = await storeImageViaSftp(sftp, PNG_BYTES).catch( + (caught: unknown) => caught, + ); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + }, 5_000); + + it("recovers a stale remote lock before writing", async () => { + const base = fakeSftp({}); + const sftp = base.sftp as ImageSftpClient; + const lockPath = `${REMOTE_IMAGE_DIR}/.termix-write-lock`; + let lockAttempt = 0; + const removed: string[] = []; + const originalMkdir = sftp.mkdir.bind(sftp); + sftp.mkdir = (dir, attrs, callback) => { + if (dir === lockPath && lockAttempt++ === 0) { + (typeof attrs === "function" ? attrs : callback!)(new Error("exists")); + return; + } + originalMkdir(dir, attrs, callback); + }; + sftp.lstat = (_dir, callback) => + callback(undefined, { + mode: 0o40700, + mtime: (Date.now() - 60_000) / 1000, + }); + sftp.rmdir = (dir, callback) => { + removed.push(dir); + callback(); + }; + + await expect(storeImageViaSftp(sftp, PNG_BYTES)).resolves.toMatchObject({ + storage: "remote-sftp", + }); + expect(removed).toContain(lockPath); + }); + + it("does not replace a successful write with an unlock failure", async () => { + const base = fakeSftp({}); + const sftp = base.sftp as ImageSftpClient; + const lockPath = `${REMOTE_IMAGE_DIR}/.termix-write-lock`; + const originalMkdir = sftp.mkdir.bind(sftp); + sftp.mkdir = (dir, attrs, callback) => originalMkdir(dir, attrs, callback); + sftp.rmdir = (dir, callback) => { + if (dir === lockPath) callback(new Error("unlock failed")); + else callback(); + }; + + const error = await storeImageViaSftp(sftp, PNG_BYTES).catch( + (caught: unknown) => caught, + ); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + }); + + it("fails closed when remote lock acquisition stalls", async () => { + const { sftp } = fakeSftp({ stallLock: true }); + const error = await storeImageViaSftp(sftp, PNG_BYTES).catch( + (caught: unknown) => caught, + ); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + }, 12_000); + + it("fails closed when remote quota inspection stalls", async () => { + const { sftp } = fakeSftp({ stallReaddir: true }); + const error = await storeImageViaSftp(sftp, PNG_BYTES, { + maxCount: 10, + maxBytes: 1024, + }).catch((caught: unknown) => caught); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + }, 7_000); + + it("fails closed when remote expiry cleanup stalls", async () => { + const { sftp } = fakeSftp({ stallReaddir: true }); + const error = await storeImageViaSftp(sftp, PNG_BYTES, { + ttlMs: 1_000, + }).catch((caught: unknown) => caught); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + }, 7_000); + + it("fails closed when remote lock release stalls", async () => { + const { sftp } = fakeSftp({ stallRmdir: true }); + const error = await storeImageViaSftp(sftp, PNG_BYTES).catch( + (caught: unknown) => caught, + ); + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + }, 5_000); + + it("maps SFTP failures to IMAGE_REMOTE_WRITE_FAILED", async () => { + const { sftp } = fakeSftp({ writeError: new Error("Permission denied") }); + const error = await storeImageViaSftp(sftp, PNG_BYTES).catch( + (caught: unknown) => caught, + ); + + expect(error).toBeInstanceOf(TerminalImageStorageError); + expect((error as TerminalImageStorageError).code).toBe( + "IMAGE_REMOTE_WRITE_FAILED", + ); + expect((error as TerminalImageStorageError).message).toBe( + "Failed to write image to the remote host", + ); + }); +}); diff --git a/src/backend/tests/database/routes/terminal-image-upload-route.test.ts b/src/backend/tests/database/routes/terminal-image-upload-route.test.ts new file mode 100644 index 0000000..8f9c2ea --- /dev/null +++ b/src/backend/tests/database/routes/terminal-image-upload-route.test.ts @@ -0,0 +1,541 @@ +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { EventEmitter } from "events"; +import fs from "fs/promises"; +import os from "os"; +import path from "path"; +import { randomUUID } from "crypto"; +import sharp from "sharp"; +import type { Request, RequestHandler, Response } from "express"; +import type { ImageSftpClient } from "../../../database/routes/terminal-image-storage.js"; +import { databaseLogger } from "../../../utils/logger.js"; + +const state = vi.hoisted(() => ({ + userId: "user-1", + settings: {} as Record, + sessions: [] as unknown[], +})); + +vi.mock("../../../utils/logger.js", () => ({ + authLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, + databaseLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, +})); + +vi.mock("../../../utils/auth-manager.js", () => ({ + AuthManager: { + getInstance: () => ({ + createAuthMiddleware: + () => (_req: unknown, _res: unknown, next: () => void) => + next(), + createDataAccessMiddleware: + () => (_req: unknown, _res: unknown, next: () => void) => + next(), + }), + }, +})); + +vi.mock("../../../hosts/terminal/session-manager.js", () => ({ + sessionManager: { + getUserSessions: () => state.sessions, + }, +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentSettingsRepository: () => ({ + get: async (key: string) => state.settings[key] ?? null, + }), + createCurrentHostResolutionRepository: () => ({}), + createCurrentCommandHistoryRepository: () => ({}), +})); + +const { default: router } = + await import("../../../database/routes/terminal.js"); + +interface RouteLayer { + route?: { + path: string; + methods: Record; + stack: Array<{ handle: RequestHandler }>; + }; +} + +const imageUploadLayer = ( + router as unknown as { stack: RouteLayer[] } +).stack.find( + (layer) => layer.route?.path === "/image-upload" && layer.route.methods.post, +); +const imageUploadHandler = + imageUploadLayer!.route!.stack[imageUploadLayer!.route!.stack.length - 1]! + .handle; + +function fakeSftp(behavior: { writeError?: Error } = {}): { + sftp: ImageSftpClient; + written: Map; + end: ReturnType; +} { + const written = new Map(); + const end = vi.fn(); + const sftp = { + mkdir: ( + _dir: string, + attrsOrCallback: { mode?: number } | ((err?: Error) => void), + maybeCallback?: (err?: Error) => void, + ) => { + const callback = + typeof attrsOrCallback === "function" + ? attrsOrCallback + : maybeCallback!; + callback(); + }, + createWriteStream: (remotePath: string, _options?: { mode?: number }) => { + const stream = new EventEmitter() as NodeJS.WritableStream & { + end: (data: Buffer) => void; + }; + stream.end = (data: Buffer) => { + queueMicrotask(() => { + if (behavior.writeError) { + stream.emit("error", behavior.writeError); + return; + } + written.set(remotePath, data); + stream.emit("close"); + }); + }; + return stream; + }, + readdir: ( + _dir: string, + callback: ( + error: Error | undefined, + entries: Array<{ + filename: string; + attrs?: { size?: number; mtime?: number }; + }>, + ) => void, + ) => callback(undefined, []), + unlink: (_path: string, callback: (error?: Error) => void) => callback(), + rmdir: (_dir: string, callback: (error?: Error) => void) => callback(), + end, + } as unknown as ImageSftpClient & { end: ReturnType }; + return { sftp, written, end }; +} + +function connectedSession(instanceId: string, sftp: ImageSftpClient) { + return { + tabInstanceId: instanceId, + isConnected: true, + sshConn: { + sftp: ( + callback: (err: Error | undefined, sftp: ImageSftpClient) => void, + ) => callback(undefined, sftp), + }, + }; +} + +async function invoke(options: { + file?: { buffer: Buffer; mimetype: string; size: number }; + instanceId?: string; + metadata?: { source?: string; clientUploadTimestamp?: string }; +}) { + const body: Record = {}; + if (options.instanceId) body.instanceId = options.instanceId; + if (options.metadata?.source !== undefined) + body.source = options.metadata.source; + if (options.metadata?.clientUploadTimestamp !== undefined) + body.clientUploadTimestamp = options.metadata.clientUploadTimestamp; + const req = { + userId: state.userId, + body, + file: options.file, + headers: {}, + } as unknown as Request; + const result = { statusCode: 200, body: null as unknown }; + const res = { + status(code: number) { + result.statusCode = code; + return this; + }, + json(body: unknown) { + result.body = body; + return this; + }, + } as unknown as Response; + await imageUploadHandler(req, res, () => {}); + return result; +} + +const LEGACY_ENV_NAMES = [ + "TERMIX_IMAGE_STORAGE_MODE", + "TERMIX_IMAGE_DIR", + "TERMIX_IMAGE_HOST_PATH", + "TERMIX_IMAGE_TTL_MS", + "TERMIX_MAX_IMAGE_COUNT", + "TERMIX_MAX_IMAGE_STORAGE_BYTES", + "DATA_DIR", +]; + +let pngBuffer: Buffer; +let savedEnv: Record; + +beforeAll(async () => { + pngBuffer = await sharp({ + create: { width: 2, height: 2, channels: 3, background: "#ffffff" }, + }) + .png() + .toBuffer(); +}); + +beforeEach(() => { + state.settings = {}; + state.sessions = []; + savedEnv = Object.fromEntries( + LEGACY_ENV_NAMES.map((name) => [name, process.env[name]]), + ); + for (const name of LEGACY_ENV_NAMES) delete process.env[name]; +}); + +afterEach(() => { + for (const name of LEGACY_ENV_NAMES) { + if (savedEnv[name] === undefined) delete process.env[name]; + else process.env[name] = savedEnv[name]; + } +}); + +describe("terminal image upload route", () => { + it("rejects requests without an image file", async () => { + const response = await invoke({ instanceId: "tab-1" }); + expect(response.statusCode).toBe(400); + expect(response.body).toMatchObject({ code: "IMAGE_FILE_MISSING" }); + }); + + it("requires a connected terminal in explicit remote-sftp mode", async () => { + state.settings["terminal_image_storage_mode"] = "remote-sftp"; + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + instanceId: "tab-1", + }); + expect(response.statusCode).toBe(409); + expect(response.body).toMatchObject({ + code: "IMAGE_TERMINAL_NOT_CONNECTED", + }); + }); + + it("writes over SFTP in auto mode when a session is connected", async () => { + const { sftp, written, end } = fakeSftp(); + state.sessions = [connectedSession("tab-1", sftp)]; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + instanceId: "tab-1", + }); + + expect(response.statusCode).toBe(200); + const body = response.body as { + id: string; + filename: string; + shellPath: string; + storage: string; + }; + expect(body.storage).toBe("remote-sftp"); + expect(body.filename).toBe(`${body.id}.png`); + expect(body.shellPath).toBe(`/tmp/termix-images/${body.filename}`); + // Sharp normalized the upload to PNG before the write. + const writtenBytes = written.get(body.shellPath)!; + expect(writtenBytes.subarray(1, 4).toString()).toBe("PNG"); + expect(end).toHaveBeenCalledTimes(1); + }); + + it("maps SFTP failures to 502 without leaking the raw remote error", async () => { + const { sftp, end } = fakeSftp({ + writeError: new Error("Permission denied"), + }); + state.sessions = [connectedSession("tab-1", sftp)]; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + instanceId: "tab-1", + }); + + expect(response.statusCode).toBe(502); + expect(response.body).toEqual({ + error: "Failed to write image to the remote host", + code: "IMAGE_REMOTE_WRITE_FAILED", + }); + expect(JSON.stringify(response.body)).not.toContain("Permission denied"); + expect(end).toHaveBeenCalledTimes(1); + }); + + it("rejects undecodable image data", async () => { + const { sftp } = fakeSftp(); + state.sessions = [connectedSession("tab-1", sftp)]; + + const response = await invoke({ + file: { + buffer: Buffer.from("definitely not an image"), + mimetype: "image/png", + size: 23, + }, + instanceId: "tab-1", + }); + + expect(response.statusCode).toBe(400); + expect(response.body).toMatchObject({ code: "IMAGE_DECODE_FAILED" }); + }); + + describe("local mapped storage", () => { + let dir: string; + + beforeEach(async () => { + // The settings validator rejects backslashes, so store the temp dir in + // POSIX form. Windows still resolves it for the real file checks. + dir = ( + await fs.mkdtemp(path.join(os.tmpdir(), "termix-route-test-")) + ).replace(/\\/g, "/"); + }); + + afterEach(async () => { + await fs.rm(dir, { recursive: true, force: true }); + }); + + it("stores locally without a terminal session and hides backend paths", async () => { + state.settings["terminal_image_storage_mode"] = "local"; + state.settings["terminal_image_local_dir"] = dir; + state.settings["terminal_image_host_path"] = "/host-view/images"; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + }); + + expect(response.statusCode).toBe(200); + const body = response.body as { + id: string; + filename: string; + shellPath: string; + storage: string; + }; + expect(body.storage).toBe("local"); + expect(body.shellPath).toBe(`/host-view/images/${body.filename}`); + expect(JSON.stringify(body)).not.toContain(dir); + const stored = await fs.readFile(path.join(dir, body.filename)); + expect(stored.subarray(1, 4).toString()).toBe("PNG"); + }); + + it("answers 507 when the configured image count cap is reached", async () => { + state.settings["terminal_image_storage_mode"] = "local"; + state.settings["terminal_image_local_dir"] = dir; + state.settings["terminal_image_host_path"] = "/host-view/images"; + state.settings["terminal_image_max_count"] = "1"; + await fs.writeFile( + path.join(dir, `${randomUUID()}.png`), + Buffer.alloc(16), + ); + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + }); + + expect(response.statusCode).toBe(507); + expect(response.body).toMatchObject({ + error: "Image storage limit reached", + code: "IMAGE_STORAGE_LIMIT_REACHED", + }); + }); + + it("keeps legacy explicit local mappings on local mode", async () => { + process.env.TERMIX_IMAGE_DIR = dir; + process.env.TERMIX_IMAGE_HOST_PATH = "/host-view/images"; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.body).toMatchObject({ storage: "local" }); + }); + + it("returns 503 when local storage inspection fails", async () => { + const blocked = `${dir}/blocked-file`; + await fs.writeFile(blocked, "not a directory"); + state.settings["terminal_image_storage_mode"] = "local"; + state.settings["terminal_image_local_dir"] = blocked; + state.settings["terminal_image_host_path"] = "/host-view/images"; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + }); + + expect(response.statusCode).toBe(503); + expect(response.body).toEqual({ + error: "Unable to inspect local image storage", + code: "IMAGE_LOCAL_INSPECTION_FAILED", + }); + expect(JSON.stringify(response.body)).not.toContain(blocked); + }); + it("rejects auto mode when no verified storage capability is available", async () => { + process.env.DATA_DIR = dir; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + instanceId: "tab-missing", + }); + + expect(response.statusCode).toBe(503); + expect(response.body).toMatchObject({ + code: "IMAGE_STORAGE_UNAVAILABLE", + }); + }); + }); + + describe("diagnostic metadata", () => { + interface UploadLogMeta { + operation?: string; + requestId?: string; + sequence?: number; + source?: string; + clientUploadTimestamp?: string; + serverReceivedAt?: string; + bytes?: number; + } + + function uploadLogEntries(): UploadLogMeta[] { + return vi + .mocked(databaseLogger.info) + .mock.calls.filter( + (call) => + (call[1] as UploadLogMeta | undefined)?.operation === + "terminal_image_upload_received", + ) + .map((call) => call[1] as UploadLogMeta); + } + + beforeEach(() => { + vi.mocked(databaseLogger.info).mockClear(); + }); + + it("logs correlation id, receipt time, and propagated source metadata", async () => { + const { sftp } = fakeSftp(); + state.sessions = [connectedSession("tab-1", sftp)]; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + instanceId: "tab-1", + metadata: { + source: "clipboard", + clientUploadTimestamp: "2026-08-15T12:00:00.000Z", + }, + }); + + expect(response.statusCode).toBe(200); + const entries = uploadLogEntries(); + expect(entries).toHaveLength(1); + const meta = entries[0]!; + expect(meta.requestId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + ); + expect(typeof meta.sequence).toBe("number"); + expect(meta.source).toBe("clipboard"); + expect(meta.clientUploadTimestamp).toBe("2026-08-15T12:00:00.000Z"); + expect(Number.isNaN(Date.parse(meta.serverReceivedAt ?? ""))).toBe(false); + expect(meta.bytes).toBe(pngBuffer.length); + }); + + it("logs a monotonically increasing upload sequence", async () => { + const { sftp } = fakeSftp(); + state.sessions = [connectedSession("tab-1", sftp)]; + + await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + instanceId: "tab-1", + metadata: { source: "file" }, + }); + await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + instanceId: "tab-1", + metadata: { source: "clipboard" }, + }); + + const sequences = uploadLogEntries().map((entry) => entry.sequence!); + expect(sequences).toHaveLength(2); + expect(sequences[1]).toBe(sequences[0]! + 1); + }); + + it("accepts missing metadata and keeps raw paths out of the log", async () => { + const dir = ( + await fs.mkdtemp(path.join(os.tmpdir(), "termix-meta-test-")) + ).replace(/\\/g, "/"); + try { + state.settings["terminal_image_storage_mode"] = "local"; + state.settings["terminal_image_local_dir"] = dir; + state.settings["terminal_image_host_path"] = "/host-view/images"; + + const response = await invoke({ + file: { + buffer: pngBuffer, + mimetype: "image/png", + size: pngBuffer.length, + }, + }); + + expect(response.statusCode).toBe(200); + const entries = uploadLogEntries(); + expect(entries).toHaveLength(1); + expect(entries[0]!.source).toBeUndefined(); + expect(entries[0]!.clientUploadTimestamp).toBeUndefined(); + expect(JSON.stringify(entries[0])).not.toContain(dir); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + }); +}); diff --git a/src/backend/tests/database/routes/terminal-image-utils.test.ts b/src/backend/tests/database/routes/terminal-image-utils.test.ts new file mode 100644 index 0000000..9837307 --- /dev/null +++ b/src/backend/tests/database/routes/terminal-image-utils.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { + exceedsImageStorageLimit, + exceedsNormalizedImageSize, + imageExtensionForFormat, + createConcurrencyLimiter, + isExpiredImage, + isImageFilename, +} from "../../../database/routes/terminal-image-utils.js"; + +describe("terminal image utilities", () => { + it("accepts UUID-based image filenames", () => { + expect(isImageFilename("b797234d-eb5d-4b1b-b7f3-17c26f257506.jpeg")).toBe( + true, + ); + }); + + it("rejects unsafe or unrelated filenames", () => { + expect(isImageFilename("../secrets.txt")).toBe(false); + expect(isImageFilename("not-an-image.jpeg")).toBe(false); + expect(isImageFilename("b797234d-eb5d-4b1b-b7f3-17c26f257506")).toBe(false); + }); + + it("maps decoded raster formats while excluding SVG", () => { + expect(imageExtensionForFormat("jpeg")).toBe("jpg"); + expect(imageExtensionForFormat("heif")).toBe("heif"); + expect(imageExtensionForFormat("tiff")).toBe("tiff"); + expect(imageExtensionForFormat("svg")).toBeUndefined(); + expect(imageExtensionForFormat(undefined)).toBeUndefined(); + }); + + it("rejects normalized output beyond the byte ceiling", () => { + expect(exceedsNormalizedImageSize(10_000_001, 10_000_000)).toBe(true); + expect(exceedsNormalizedImageSize(10_000_000, 10_000_000)).toBe(false); + }); + it("expires files older than the configured TTL", () => { + expect(isExpiredImage(1_000, 3_000, 1_000)).toBe(true); + expect(isExpiredImage(2_500, 3_000, 1_000)).toBe(false); + }); + + it("treats a zero TTL as retention disabled", () => { + expect(isExpiredImage(1_000, 999_999, 0)).toBe(false); + }); + + it("bounds the admission queue", async () => { + const limiter = createConcurrencyLimiter(1, 0); + const release = await limiter.acquire(); + await expect(limiter.acquire()).rejects.toThrow("queue is full"); + release(); + }); + + it("rejects uploads that exceed the count or byte limit", () => { + expect(exceedsImageStorageLimit(100, 10, 1, 100, 1000)).toBe(true); + expect(exceedsImageStorageLimit(1, 900, 101, 100, 1000)).toBe(true); + expect(exceedsImageStorageLimit(1, 900, 100, 100, 1000)).toBe(false); + }); + + it("bounds concurrent work", async () => { + const limiter = createConcurrencyLimiter(1); + let active = 0; + let peak = 0; + const task = async () => { + const release = await limiter.acquire(); + active += 1; + peak = Math.max(peak, active); + await new Promise((resolve) => setTimeout(resolve, 5)); + active -= 1; + release(); + }; + + await Promise.all([task(), task(), task()]); + + expect(peak).toBe(1); + expect(limiter.active).toBe(0); + }); +}); diff --git a/src/backend/tests/database/routes/totp-disable-route.test.ts b/src/backend/tests/database/routes/totp-disable-route.test.ts new file mode 100644 index 0000000..4ecfcd0 --- /dev/null +++ b/src/backend/tests/database/routes/totp-disable-route.test.ts @@ -0,0 +1,153 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import bcrypt from "bcryptjs"; +import speakeasy from "speakeasy"; + +const userUpdate = vi.fn().mockResolvedValue(null); +const findById = vi.fn(); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentUserRepository: () => ({ findById, update: userUpdate }), + createCurrentTrustedDeviceRepository: () => ({}), + createCurrentUserSessionRepository: () => ({}), +})); + +vi.mock("../../../utils/logger.js", () => ({ + authLogger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + success: vi.fn(), + }, +})); + +vi.mock("../../../utils/database-save-trigger.js", () => ({ + DatabaseSaveTrigger: { forceSave: vi.fn().mockResolvedValue(undefined) }, +})); + +const { registerUserTotpRoutes } = + await import("../../../database/routes/user-totp-routes.js"); + +const secret = speakeasy.generateSecret({ name: "test" }).base32; +const PASSWORD = "correct-horse"; + +/** + * The disable dialog has one field, labelled "Enter TOTP code or password", + * and its caller passes that single value as `disableTOTP(input)` โ€” which + * lands in the `password` argument, leaving `totp_code` undefined. + * + * 2.5.1 changed the route to require both, so from then on the first check + * rejected every attempt regardless of what was typed. Nobody could turn 2FA + * off, and the client reported it as the generic "Failed to disable 2FA". + */ +describe("POST /totp/disable", () => { + let handler: (req: unknown, res: unknown) => Promise; + + beforeEach(() => { + vi.clearAllMocks(); + + const routes = new Map unknown>(); + const router = { + post: (path: string, ...rest: unknown[]) => { + routes.set(path, rest[rest.length - 1] as never); + }, + get: () => {}, + put: () => {}, + delete: () => {}, + }; + + registerUserTotpRoutes( + router as never, + { + authenticateJWT: (() => {}) as never, + authManager: { getUserDataKey: () => null } as never, + isNativeAppRequest: () => false, + } as never, + ); + + handler = routes.get("/totp/disable") as never; + findById.mockResolvedValue({ + id: "user-1", + isOidc: false, + passwordHash: bcrypt.hashSync(PASSWORD, 4), + totpSecret: secret, + totpBackupCodes: JSON.stringify(["BACKUP01"]), + totpEnabled: true, + }); + }); + + function call(body: Record) { + const res = { + statusCode: 200, + body: undefined as unknown, + status(code: number) { + this.statusCode = code; + return this; + }, + json(payload: unknown) { + this.body = payload; + return this; + }, + }; + return handler({ userId: "user-1", body }, res).then(() => res); + } + + it("accepts the TOTP code on its own", async () => { + // What the dialog sends: one value, in whichever field the client used. + const res = await call({ + totp_code: speakeasy.totp({ secret, encoding: "base32" }), + }); + + expect(res.statusCode).toBe(200); + expect(userUpdate).toHaveBeenCalledWith( + "user-1", + expect.objectContaining({ totpEnabled: false, totpSecret: null }), + ); + }); + + it("accepts the account password on its own", async () => { + // The single field is labelled "TOTP code or password", and the client + // passes it as the password argument โ€” this is the exact failing call. + const res = await call({ password: PASSWORD }); + + expect(res.statusCode).toBe(200); + expect(userUpdate).toHaveBeenCalled(); + }); + + it("accepts a backup code", async () => { + const res = await call({ totp_code: "BACKUP01" }); + + expect(res.statusCode).toBe(200); + }); + + it("still refuses a wrong value", async () => { + const res = await call({ password: "not-my-password" }); + + expect(res.statusCode).toBe(401); + expect(userUpdate).not.toHaveBeenCalled(); + }); + + it("refuses an empty request rather than disabling anything", async () => { + const res = await call({}); + + expect(res.statusCode).toBe(400); + expect(userUpdate).not.toHaveBeenCalled(); + }); + + it("does not let an OIDC user disable it with a password", async () => { + // No password to compare against; only a TOTP or backup code will do. + findById.mockResolvedValue({ + id: "user-1", + isOidc: true, + passwordHash: null, + totpSecret: secret, + totpBackupCodes: JSON.stringify([]), + totpEnabled: true, + }); + + const res = await call({ password: "anything" }); + + expect(res.statusCode).toBe(401); + expect(userUpdate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/database/routes/touch-input-settings-routes.test.ts b/src/backend/tests/database/routes/touch-input-settings-routes.test.ts new file mode 100644 index 0000000..83577b9 --- /dev/null +++ b/src/backend/tests/database/routes/touch-input-settings-routes.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Request, RequestHandler, Response } from "express"; +import { TOUCH_INPUT_DEFAULTS } from "../../../../types/touch-input-settings.js"; + +const state = vi.hoisted(() => ({ + userId: "admin", + admins: new Set(["admin"]), + stored: null as string | null, +})); + +vi.mock("../../../utils/logger.js", () => ({ + authLogger: { error: vi.fn() }, +})); +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentUserRepository: () => ({ + findById: async (id: string) => ({ id, isAdmin: state.admins.has(id) }), + }), + createCurrentSettingsRepository: () => ({ + get: async () => state.stored, + set: async (_key: string, value: string) => { + state.stored = value; + }, + }), +})); + +const { registerTouchInputSettingsRoutes } = + await import("../../../database/routes/touch-input-settings-routes.js"); + +type Registered = { method: string; path: string; handler: RequestHandler }; +const registered: Registered[] = []; +const router = { + get: (path: string, ...handlers: RequestHandler[]) => + registered.push({ method: "get", path, handler: handlers.at(-1)! }), + patch: (path: string, ...handlers: RequestHandler[]) => + registered.push({ method: "patch", path, handler: handlers.at(-1)! }), +} as unknown as import("express").Router; +registerTouchInputSettingsRoutes(router, (_req, _res, next) => next()); + +async function invoke(method: string, body: unknown = {}) { + const handler = registered.find((entry) => entry.method === method)!.handler; + const req = { userId: state.userId, body } as unknown as Request; + const result = { statusCode: 200, body: null as unknown }; + const res = { + status(code: number) { + result.statusCode = code; + return this; + }, + json(bodyValue: unknown) { + result.body = bodyValue; + return this; + }, + } as Response; + await handler(req, res, () => {}); + return result; +} + +beforeEach(() => { + state.userId = "admin"; + state.stored = null; +}); + +describe("touch input settings routes", () => { + it("allows authenticated non-admin users to read normalized defaults", async () => { + state.userId = "user"; + const response = await invoke("get"); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual(TOUCH_INPUT_DEFAULTS); + }); + + it("only allows admins to write", async () => { + state.userId = "user"; + const response = await invoke("patch", { enabled: false }); + expect(response.statusCode).toBe(403); + expect(state.stored).toBeNull(); + }); + + it("rejects out-of-range values and persists normalized updates", async () => { + const invalid = await invoke("patch", { maximumTicksPerFrame: 101 }); + expect(invalid.statusCode).toBe(400); + + const valid = await invoke("patch", { + dragThresholdPx: 9, + momentumEnabled: false, + }); + expect(valid.statusCode).toBe(200); + expect(JSON.parse(state.stored!)).toEqual({ + ...TOUCH_INPUT_DEFAULTS, + dragThresholdPx: 9, + momentumEnabled: false, + }); + }); +}); diff --git a/src/backend/tests/database/routes/user-admin-routes.test.ts b/src/backend/tests/database/routes/user-admin-routes.test.ts index 22efb67..4f52373 100644 --- a/src/backend/tests/database/routes/user-admin-routes.test.ts +++ b/src/backend/tests/database/routes/user-admin-routes.test.ts @@ -54,6 +54,28 @@ vi.mock("../../../utils/auth-manager.js", () => ({ vi.mock("../../../database/repositories/factory.js", () => ({ createCurrentUserRepository: () => ({ listAll: async () => [...state.users.values()], + listPage: async ({ + search, + limit, + offset, + }: { + search?: string; + limit: number; + offset: number; + }) => { + const term = search?.trim().toLowerCase(); + const matched = [...state.users.values()] + .filter((u) => !term || u.username?.toLowerCase().includes(term)) + .sort((a, b) => + (a.username ?? "").localeCompare(b.username ?? "", undefined, { + sensitivity: "base", + }), + ); + return { + users: matched.slice(offset, offset + limit), + total: matched.length, + }; + }, findById: async (id: string) => state.users.get(id) ?? null, findByUsername: async (username: string) => [...state.users.values()].find((u) => u.username === username) ?? null, @@ -102,11 +124,13 @@ function findHandler(method: string, path: string): RequestHandler { function makeReqRes(overrides: { body?: Record; params?: Record; + query?: Record; }) { const req = { userId: state.currentUserId, body: overrides.body ?? {}, params: overrides.params ?? {}, + query: overrides.query ?? {}, headers: {}, } as unknown as Request; @@ -142,6 +166,7 @@ async function invoke( overrides: { body?: Record; params?: Record; + query?: Record; } = {}, ) { const handler = findHandler(method, path); @@ -214,6 +239,64 @@ describe("GET /list", () => { expect(users[0].data_unlocked).toBeUndefined(); expect(users[0].totp_enabled).toBeUndefined(); }); + + it("returns every user when no limit is given", async () => { + // The share pickers depend on this: they fetch once and filter locally. + const res = await invoke("get", "/list"); + const body = res.jsonBody as { + users: unknown[]; + limit?: number; + total: number; + }; + expect(body.users).toHaveLength(3); + expect(body.total).toBe(3); + expect(body.limit).toBeUndefined(); + }); + + it("returns one page and the full total when a limit is given", async () => { + const res = await invoke("get", "/list", { query: { limit: "2" } }); + const body = res.jsonBody as { + users: unknown[]; + total: number; + limit: number; + offset: number; + }; + expect(body.users).toHaveLength(2); + expect(body.total).toBe(3); + expect(body.limit).toBe(2); + expect(body.offset).toBe(0); + }); + + it("pages with an offset", async () => { + const res = await invoke("get", "/list", { + query: { limit: "2", offset: "2" }, + }); + const body = res.jsonBody as { users: unknown[]; total: number }; + expect(body.users).toHaveLength(1); + expect(body.total).toBe(3); + }); + + it("filters by search term without a limit", async () => { + const res = await invoke("get", "/list", { query: { search: "lock" } }); + const body = res.jsonBody as { + users: { username: string }[]; + total: number; + }; + expect(body.users.map((u) => u.username)).toEqual(["locked"]); + expect(body.total).toBe(1); + }); + + it("caps an oversized page size", async () => { + const res = await invoke("get", "/list", { query: { limit: "100000" } }); + expect((res.jsonBody as { limit: number }).limit).toBe(500); + }); + + it("ignores a non-numeric limit and returns the full list", async () => { + const res = await invoke("get", "/list", { query: { limit: "abc" } }); + const body = res.jsonBody as { users: unknown[]; limit?: number }; + expect(body.users).toHaveLength(3); + expect(body.limit).toBeUndefined(); + }); }); describe("POST /admin/totp/disable", () => { diff --git a/src/backend/tests/database/routes/user-oidc-utils.test.ts b/src/backend/tests/database/routes/user-oidc-utils.test.ts index 96dcc72..d56aeff 100644 --- a/src/backend/tests/database/routes/user-oidc-utils.test.ts +++ b/src/backend/tests/database/routes/user-oidc-utils.test.ts @@ -15,12 +15,191 @@ const { isOIDCUserAllowed, getOIDCConfigFromEnv, extractOidcGroups, + extractOidcGroupsFromSources, validateLogoutTokenClaims, + parseOidcRoleMap, + resolveOidcMappedRoles, + verifyOIDCToken, + describeFetchFailure, } = await import("../../../database/routes/user-oidc-utils.js"); const BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout"; +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("describeFetchFailure", () => { + it("unwraps the undici cause, which carries the reason that matters", () => { + // Every transport failure surfaces as this same outer message. + const error = new TypeError("fetch failed", { + cause: Object.assign(new Error("getaddrinfo ENOTFOUND idp.example"), { + code: "ENOTFOUND", + }), + }); + expect(describeFetchFailure(error)).toBe( + "fetch failed: getaddrinfo ENOTFOUND idp.example (ENOTFOUND)", + ); + }); + + it("falls back to the outer message when there is no cause", () => { + expect(describeFetchFailure(new Error("boom"))).toBe("boom"); + }); + + it("handles a non-Error throw", () => { + expect(describeFetchFailure("nope")).toBe("nope"); + }); +}); + +describe("verifyOIDCToken JWKS diagnostics", () => { + const issuer = "https://login.microsoftonline.com/example/v2.0"; + const token = "header.payload.signature"; + + it("reports every attempted URL and why it failed", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("not found", { status: 404 }), + ); + + const error = await verifyOIDCToken(token, issuer, "client").catch( + (e) => e as Error, + ); + expect(error.message).toMatch(/^Failed to fetch JWKS from any URL/); + expect(error.message).toContain( + `${issuer}/.well-known/openid-configuration: HTTP 404`, + ); + expect(error.message).toContain( + `${issuer}/.well-known/jwks.json: HTTP 404`, + ); + expect(error.message).toContain(`${issuer}/jwks/: HTTP 404`); + }); + + it("reports a transport failure with its underlying cause", async () => { + vi.spyOn(globalThis, "fetch").mockRejectedValue( + new TypeError("fetch failed", { + cause: Object.assign(new Error("self-signed certificate"), { + code: "SELF_SIGNED_CERT_IN_CHAIN", + }), + }), + ); + + const error = await verifyOIDCToken(token, issuer, "client").catch( + (e) => e as Error, + ); + expect(error.message).toContain( + "self-signed certificate (SELF_SIGNED_CERT_IN_CHAIN)", + ); + }); + + it("says so when discovery succeeds but advertises no jwks_uri", async () => { + vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response(JSON.stringify({ issuer }), { status: 200 }), + ) + .mockResolvedValue(new Response("not found", { status: 404 })); + + const error = await verifyOIDCToken(token, issuer, "client").catch( + (e) => e as Error, + ); + expect(error.message).toContain("no jwks_uri in the discovery document"); + }); + + it("says so when a JWKS response carries no keys array", async () => { + vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response(JSON.stringify({ jwks_uri: "https://idp.example/keys" }), { + status: 200, + }), + ) + .mockResolvedValue( + new Response(JSON.stringify({ error: "unauthorized" }), { + status: 200, + }), + ); + + const error = await verifyOIDCToken(token, issuer, "client").catch( + (e) => e as Error, + ); + expect(error.message).toContain( + 'https://idp.example/keys: response contains no "keys" array', + ); + }); +}); + +describe("verifyOIDCToken", () => { + it("accepts a discovery document URL as the configured issuer", async () => { + const { exportJWK, generateKeyPair, SignJWT } = await import("jose"); + const { publicKey, privateKey } = await generateKeyPair("RS256"); + const jwk = await exportJWK(publicKey); + jwk.kid = "google-key"; + + const issuer = "https://accounts.google.com"; + const clientId = "termix-client"; + const token = await new SignJWT({ sub: "user-1" }) + .setProtectedHeader({ alg: "RS256", kid: jwk.kid }) + .setIssuer(issuer) + .setAudience(clientId) + .setExpirationTime("5m") + .sign(privateKey); + + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response(JSON.stringify({ jwks_uri: `${issuer}/keys` }), { + status: 200, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ keys: [jwk] }), { status: 200 }), + ); + + const payload = await verifyOIDCToken( + token, + `${issuer}/.well-known/openid-configuration`, + clientId, + ); + + expect(payload.sub).toBe("user-1"); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + `${issuer}/.well-known/openid-configuration`, + {}, + ); + }); + + it("uses the protected-header algorithm when the provider JWK omits alg", async () => { + const { exportJWK, generateKeyPair, SignJWT } = await import("jose"); + const { publicKey, privateKey } = await generateKeyPair("RS256"); + const jwk = await exportJWK(publicKey); + jwk.kid = "entra-key"; + + const issuer = "https://login.microsoftonline.com/example/v2.0"; + const clientId = "termix-client"; + const token = await new SignJWT({ sub: "user-1" }) + .setProtectedHeader({ alg: "RS256", kid: jwk.kid }) + .setIssuer(issuer) + .setAudience(clientId) + .setExpirationTime("5m") + .sign(privateKey); + + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response(JSON.stringify({ jwks_uri: "https://idp.example/keys" }), { + status: 200, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ keys: [jwk] }), { status: 200 }), + ); + + const payload = await verifyOIDCToken(token, issuer, clientId); + + expect(payload.sub).toBe("user-1"); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); +}); + describe("isOIDCUserAllowed", () => { it("allows everyone when the allow-list is empty", () => { expect(isOIDCUserAllowed("", "alice", "alice@x.com")).toBe(true); @@ -207,6 +386,35 @@ describe("extractOidcGroups", () => { }); }); +describe("extractOidcGroupsFromSources", () => { + it("preserves ID token groups when userinfo omits them", () => { + expect( + extractOidcGroupsFromSources([ + { groups: ["admins", "users"] }, + { sub: "user-1", name: "Example User" }, + ]), + ).toEqual(["admins", "users"]); + }); + + it("combines and deduplicates groups from both verified sources", () => { + expect( + extractOidcGroupsFromSources([ + { roles: ["users", "operators"] }, + { roles: ["operators", "admins"] }, + ]), + ).toEqual(["users", "operators", "admins"]); + }); + + it("supports a configured group claim across sources", () => { + expect( + extractOidcGroupsFromSources( + [{ custom_groups: ["admins"] }, { custom_groups: ["users"] }], + "custom_groups", + ), + ).toEqual(["admins", "users"]); + }); +}); + describe("validateLogoutTokenClaims", () => { const validClaims = { sub: "subject-1", @@ -251,3 +459,143 @@ describe("validateLogoutTokenClaims", () => { ).toThrow("must contain sub and/or sid"); }); }); + +describe("parseOidcRoleMap", () => { + it("returns an empty map for blank input", () => { + expect(parseOidcRoleMap(undefined).size).toBe(0); + expect(parseOidcRoleMap(null).size).toBe(0); + expect(parseOidcRoleMap(" ").size).toBe(0); + }); + + it("parses comma-separated group:role pairs", () => { + const map = parseOidcRoleMap( + "devops-interns:devops-intern,devops-seniors:devops-senior", + ); + expect(map.get("devops-interns")).toBe("devops-intern"); + expect(map.get("devops-seniors")).toBe("devops-senior"); + expect(map.size).toBe(2); + }); + + it("parses newline-separated pairs and trims whitespace", () => { + const map = parseOidcRoleMap(" a : role-a \n b:role-b \n"); + expect(map.get("a")).toBe("role-a"); + expect(map.get("b")).toBe("role-b"); + }); + + it("normalizes leading slashes and case in group names", () => { + const map = parseOidcRoleMap("/DevOps-Interns:devops-intern"); + expect(map.get("devops-interns")).toBe("devops-intern"); + }); + + it("skips malformed entries instead of throwing", () => { + const map = parseOidcRoleMap("no-colon,:missing-group,missing-role:,ok:r"); + expect(map.size).toBe(1); + expect(map.get("ok")).toBe("r"); + }); + + it("splits on the last colon so group names may contain colons", () => { + const map = parseOidcRoleMap("ns:team:role-x"); + expect(map.get("ns:team")).toBe("role-x"); + }); + + it("preserves role-name case verbatim", () => { + // Role names must match roles.name exactly, so they are not lowercased. + expect(parseOidcRoleMap("g:DevOps_Senior").get("g")).toBe("DevOps_Senior"); + }); +}); + +describe("resolveOidcMappedRoles", () => { + const roleMap = parseOidcRoleMap( + "devops-interns:devops-intern,devops-seniors:devops-senior", + ); + + it("reports every mapped role as managed regardless of membership", () => { + const { managed } = resolveOidcMappedRoles([], roleMap); + expect([...managed].sort()).toEqual(["devops-intern", "devops-senior"]); + }); + + it("desires only the roles whose groups the user is in", () => { + const { desired } = resolveOidcMappedRoles(["devops-interns"], roleMap); + expect([...desired]).toEqual(["devops-intern"]); + }); + + it("matches full group paths emitted by Keycloak", () => { + const { desired } = resolveOidcMappedRoles(["/devops-seniors"], roleMap); + expect([...desired]).toEqual(["devops-senior"]); + }); + + it("ignores groups that are not mapped", () => { + const { desired } = resolveOidcMappedRoles( + ["finance", "devops-interns"], + roleMap, + ); + expect([...desired]).toEqual(["devops-intern"]); + }); + + it("supports a user in multiple mapped groups", () => { + const { desired } = resolveOidcMappedRoles( + ["devops-interns", "devops-seniors"], + roleMap, + ); + expect([...desired].sort()).toEqual(["devops-intern", "devops-senior"]); + }); + + it("desires nothing when the map is empty", () => { + const { desired, managed } = resolveOidcMappedRoles( + ["devops-interns"], + new Map(), + ); + expect(desired.size).toBe(0); + expect(managed.size).toBe(0); + }); +}); + +// Imported as a namespace rather than destructured into the shared block at the +// top of the file, so this suite stays independent of what that block binds. +const oidcUtils = await import("../../../database/routes/user-oidc-utils.js"); + +describe("verifyOIDCToken token shape", () => { + const issuer = "https://idp.example.com/application/o/termix"; + + // The shape check runs before any network call, so no fetch stub is needed. + const fetchSpy = vi.fn(); + beforeEach(() => { + vi.stubGlobal("fetch", fetchSpy); + }); + afterEach(() => { + vi.unstubAllGlobals(); + fetchSpy.mockReset(); + }); + + it("reports an encrypted (JWE) token as a format error", async () => { + const jwe = ["header", "key", "iv", "ciphertext", "tag"].join("."); + + await expect( + oidcUtils.verifyOIDCToken(jwe, issuer, "client"), + ).rejects.toThrow(oidcUtils.OIDCTokenFormatError); + await expect( + oidcUtils.verifyOIDCToken(jwe, issuer, "client"), + ).rejects.toThrow(/JWE \(encrypted\)/); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("reports any other non-JWS segment count as a format error", async () => { + await expect( + oidcUtils.verifyOIDCToken("header.payload", issuer, "client"), + ).rejects.toThrow(/expected 3 segments, got 2/); + await expect( + oidcUtils.verifyOIDCToken("opaque", issuer, "client"), + ).rejects.toThrow(/expected 3 segments, got 1/); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("lets a three-segment token through to key resolution", async () => { + fetchSpy.mockResolvedValue({ ok: false }); + + // Reaches JWKS fetching, so it fails on the key lookup rather than the shape. + await expect( + oidcUtils.verifyOIDCToken("header.payload.signature", issuer, "client"), + ).rejects.not.toThrow(oidcUtils.OIDCTokenFormatError); + expect(fetchSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/database/routes/user-preferences.test.ts b/src/backend/tests/database/routes/user-preferences.test.ts new file mode 100644 index 0000000..1ee4d38 --- /dev/null +++ b/src/backend/tests/database/routes/user-preferences.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { validateDefaultsJson } from "../../../database/routes/user-preferences.js"; + +describe("user connection defaults validation", () => { + it("accepts JSON objects", () => { + expect(validateDefaultsJson('{"fontSize":16}')).toBe(true); + expect(validateDefaultsJson("{}")).toBe(true); + }); + + it("rejects malformed JSON and non-object values", () => { + expect(validateDefaultsJson("{")).toBe(false); + expect(validateDefaultsJson("null")).toBe(false); + expect(validateDefaultsJson("[]")).toBe(false); + }); + + it("rejects payloads larger than 32 KiB", () => { + expect( + validateDefaultsJson(JSON.stringify({ value: "x".repeat(32_768) })), + ).toBe(false); + }); +}); diff --git a/src/backend/tests/database/routes/workspaces.test.ts b/src/backend/tests/database/routes/workspaces.test.ts new file mode 100644 index 0000000..81c3ceb --- /dev/null +++ b/src/backend/tests/database/routes/workspaces.test.ts @@ -0,0 +1,511 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { Request, Response, Router } from "express"; + +type WorkspaceRow = { + id: number; + userId: string; + name: string; + color: string | null; + icon: string | null; + kind: "manual" | "last_session"; + isDefault: boolean; + payload: string; + syncId: string; + createdAt: string; + updatedAt: string; + lastUsedAt: string | null; +}; + +const state = vi.hoisted(() => ({ + currentUserId: "user-1", + workspaces: new Map(), + nextId: 1, +})); + +vi.mock("../../../database/db/index.js", () => ({ db: {} })); + +vi.mock("../../../utils/logger.js", () => ({ + databaseLogger: { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + success: vi.fn(), + }, +})); + +vi.mock("../../../utils/auth-manager.js", () => ({ + AuthManager: { + getInstance: () => ({ + createAuthMiddleware: + () => + (req: Record, _res: unknown, next: () => void) => { + req.userId = state.currentUserId; + next(); + }, + createDataAccessMiddleware: + () => (_req: unknown, _res: unknown, next: () => void) => + next(), + }), + }, +})); + +function findByIdForUser(userId: string, id: number): WorkspaceRow | null { + const row = state.workspaces.get(id); + return row && row.userId === userId ? row : null; +} + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentWorkspaceRepository: () => ({ + listByUser: async (userId: string) => + [...state.workspaces.values()].filter((w) => w.userId === userId), + findById: async (userId: string, id: number) => findByIdForUser(userId, id), + findLastSession: async (userId: string) => + [...state.workspaces.values()].find( + (w) => w.userId === userId && w.kind === "last_session", + ) ?? null, + upsertLastSession: async (userId: string, payload: string) => { + const existing = [...state.workspaces.values()].find( + (w) => w.userId === userId && w.kind === "last_session", + ); + if (existing) { + existing.payload = payload; + existing.updatedAt = new Date().toISOString(); + return existing; + } + const row: WorkspaceRow = { + id: state.nextId++, + userId, + name: "Last Session", + color: null, + icon: null, + kind: "last_session", + isDefault: false, + payload, + syncId: `sync-${state.nextId}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + lastUsedAt: null, + }; + state.workspaces.set(row.id, row); + return row; + }, + create: async ( + userId: string, + input: { + name: string; + color?: string | null; + icon?: string | null; + payload: string; + }, + ) => { + const row: WorkspaceRow = { + id: state.nextId++, + userId, + name: input.name, + color: input.color ?? null, + icon: input.icon ?? null, + kind: "manual", + isDefault: false, + payload: input.payload, + syncId: `sync-${state.nextId}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + lastUsedAt: null, + }; + state.workspaces.set(row.id, row); + return row; + }, + update: async ( + userId: string, + id: number, + input: { name?: string; color?: string | null; icon?: string | null }, + ) => { + const row = findByIdForUser(userId, id); + if (!row || row.kind !== "manual") return null; + if (input.name !== undefined) row.name = input.name; + if (input.color !== undefined) row.color = input.color; + if (input.icon !== undefined) row.icon = input.icon; + return row; + }, + updateContent: async (userId: string, id: number, payload: string) => { + const row = findByIdForUser(userId, id); + if (!row || row.kind !== "manual") return null; + row.payload = payload; + return row; + }, + setDefault: async (userId: string, id: number) => { + const row = findByIdForUser(userId, id); + if (!row || row.kind !== "manual") return null; + for (const w of state.workspaces.values()) { + if (w.userId === userId && w.kind === "manual") w.isDefault = false; + } + row.isDefault = true; + return row; + }, + unsetDefault: async (userId: string, id: number) => { + const row = findByIdForUser(userId, id); + if (!row || row.kind !== "manual") return null; + row.isDefault = false; + return row; + }, + touchLastUsed: async (userId: string, id: number) => { + const row = findByIdForUser(userId, id); + if (row) row.lastUsedAt = new Date().toISOString(); + }, + delete: async (userId: string, id: number) => { + const row = findByIdForUser(userId, id); + if (!row || row.kind !== "manual") return false; + state.workspaces.delete(id); + return true; + }, + }), +})); + +const { default: router } = + await import("../../../database/routes/workspaces.js"); + +function findLayer(method: string, path: string) { + const stack = (router as unknown as Router).stack as Array<{ + route?: { + path: string; + methods: Record; + stack: Array<{ handle: (req: Request, res: Response) => unknown }>; + }; + }>; + const layer = stack.find( + (l) => l.route?.path === path && l.route?.methods[method], + ); + if (!layer?.route) throw new Error(`No route for ${method} ${path}`); + return layer.route.stack[layer.route.stack.length - 1].handle; +} + +function makeReqRes(overrides: { + body?: Record; + params?: Record; +}) { + const req = { + userId: state.currentUserId, + body: overrides.body ?? {}, + params: overrides.params ?? {}, + headers: {}, + } as unknown as Request; + + const res = { + statusCode: 200, + jsonBody: null as unknown, + status(code: number) { + (this as unknown as { statusCode: number }).statusCode = code; + return this; + }, + json(payload: unknown) { + (this as unknown as { jsonBody: unknown }).jsonBody = payload; + return this; + }, + } as unknown as Response & { statusCode: number; jsonBody: unknown }; + + return { req, res }; +} + +async function invoke( + method: string, + path: string, + overrides: { + body?: Record; + params?: Record; + } = {}, +) { + const handler = findLayer(method, path); + const { req, res } = makeReqRes(overrides); + await handler(req, res); + return res as unknown as { + statusCode: number; + jsonBody: Record | null; + }; +} + +beforeEach(() => { + state.currentUserId = "user-1"; + state.workspaces = new Map(); + state.nextId = 1; +}); + +describe("GET /", () => { + it("returns the list with a computed tabCount", async () => { + await invoke("post", "/", { + body: { + name: "Test A", + payload: { version: 1, tabs: [{ slotId: "a" }, { slotId: "b" }] }, + }, + }); + + const res = await invoke("get", "/"); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toHaveLength(1); + expect( + (res.jsonBody as unknown as { tabCount: number }[])[0].tabCount, + ).toBe(2); + }); +}); + +describe("POST /", () => { + it("400s when name is missing", async () => { + const res = await invoke("post", "/", { + body: { payload: { version: 1, tabs: [] } }, + }); + expect(res.statusCode).toBe(400); + }); + + it("400s when payload has no tabs array", async () => { + const res = await invoke("post", "/", { + body: { name: "Test A", payload: { version: 1 } }, + }); + expect(res.statusCode).toBe(400); + }); + + it("200s and creates a manual workspace", async () => { + const res = await invoke("post", "/", { + body: { name: "Test A", payload: { version: 1, tabs: [] } }, + }); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toMatchObject({ name: "Test A", kind: "manual" }); + }); +}); + +describe("PATCH /:id", () => { + it("rejects renaming the last_session row", async () => { + const created = await invoke("put", "/last-session", { + body: { payload: { version: 1, tabs: [] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const res = await invoke("patch", "/:id", { + params: { id: String(id) }, + body: { name: "Nope" }, + }); + expect(res.statusCode).toBe(404); + }); + + it("renames a manual workspace", async () => { + const created = await invoke("post", "/", { + body: { name: "Old", payload: { version: 1, tabs: [] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const res = await invoke("patch", "/:id", { + params: { id: String(id) }, + body: { name: "New" }, + }); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toMatchObject({ name: "New" }); + }); + + it("404s for a nonexistent id", async () => { + const res = await invoke("patch", "/:id", { + params: { id: "999" }, + body: { name: "New" }, + }); + expect(res.statusCode).toBe(404); + }); +}); + +describe("PUT /:id/content", () => { + it("updates payload for a manual workspace", async () => { + const created = await invoke("post", "/", { + body: { name: "A", payload: { version: 1, tabs: [] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const res = await invoke("put", "/:id/content", { + params: { id: String(id) }, + body: { payload: { version: 1, tabs: [{ slotId: "x" }] } }, + }); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toMatchObject({ tabCount: 1 }); + }); + + it("404s on wrong owner", async () => { + const created = await invoke("post", "/", { + body: { name: "A", payload: { version: 1, tabs: [] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + state.currentUserId = "user-2"; + const res = await invoke("put", "/:id/content", { + params: { id: String(id) }, + body: { payload: { version: 1, tabs: [] } }, + }); + expect(res.statusCode).toBe(404); + }); +}); + +describe("DELETE /:id", () => { + it("rejects deleting the last_session row", async () => { + const created = await invoke("put", "/last-session", { + body: { payload: { version: 1, tabs: [] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const res = await invoke("delete", "/:id", { params: { id: String(id) } }); + expect(res.statusCode).toBe(404); + }); + + it("404s for a nonexistent id", async () => { + const res = await invoke("delete", "/:id", { params: { id: "999" } }); + expect(res.statusCode).toBe(404); + }); + + it("deletes a manual workspace", async () => { + const created = await invoke("post", "/", { + body: { name: "A", payload: { version: 1, tabs: [] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const res = await invoke("delete", "/:id", { params: { id: String(id) } }); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toEqual({ success: true }); + }); +}); + +describe("POST /:id/duplicate", () => { + it("produces a second independent row", async () => { + const created = await invoke("post", "/", { + body: { + name: "A", + payload: { version: 1, tabs: [{ slotId: "a" }] }, + }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const dup = await invoke("post", "/:id/duplicate", { + params: { id: String(id) }, + body: { name: "A (copy)" }, + }); + expect(dup.statusCode).toBe(200); + expect(dup.jsonBody).toMatchObject({ name: "A (copy)", tabCount: 1 }); + + const updateOriginal = await invoke("put", "/:id/content", { + params: { id: String(id) }, + body: { payload: { version: 1, tabs: [] } }, + }); + expect(updateOriginal.jsonBody).toMatchObject({ tabCount: 0 }); + + const dupId = (dup.jsonBody as unknown as { id: number }).id; + const refetched = await invoke("get", "/"); + const dupRow = ( + refetched.jsonBody as unknown as { id: number; tabCount: number }[] + ).find((w) => w.id === dupId); + expect(dupRow?.tabCount).toBe(1); + }); +}); + +describe("POST /:id/set-default", () => { + it("clears a previous default and sets the new one", async () => { + const a = await invoke("post", "/", { + body: { name: "A", payload: { version: 1, tabs: [] } }, + }); + const b = await invoke("post", "/", { + body: { name: "B", payload: { version: 1, tabs: [] } }, + }); + const aId = (a.jsonBody as unknown as { id: number }).id; + const bId = (b.jsonBody as unknown as { id: number }).id; + + await invoke("post", "/:id/set-default", { params: { id: String(aId) } }); + const second = await invoke("post", "/:id/set-default", { + params: { id: String(bId) }, + }); + expect(second.jsonBody).toMatchObject({ isDefault: true }); + + const list = await invoke("get", "/"); + const aRow = ( + list.jsonBody as unknown as { id: number; isDefault: boolean }[] + ).find((w) => w.id === aId); + expect(aRow?.isDefault).toBe(false); + }); + + it("is idempotent when re-called on an already-default row", async () => { + const a = await invoke("post", "/", { + body: { name: "A", payload: { version: 1, tabs: [] } }, + }); + const aId = (a.jsonBody as unknown as { id: number }).id; + + await invoke("post", "/:id/set-default", { params: { id: String(aId) } }); + const again = await invoke("post", "/:id/set-default", { + params: { id: String(aId) }, + }); + expect(again.statusCode).toBe(200); + expect(again.jsonBody).toMatchObject({ isDefault: true }); + }); +}); + +describe("POST /:id/unset-default", () => { + it("clears isDefault on a manual workspace", async () => { + const a = await invoke("post", "/", { + body: { name: "A", payload: { version: 1, tabs: [] } }, + }); + const aId = (a.jsonBody as unknown as { id: number }).id; + + await invoke("post", "/:id/set-default", { params: { id: String(aId) } }); + const res = await invoke("post", "/:id/unset-default", { + params: { id: String(aId) }, + }); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toMatchObject({ isDefault: false }); + }); + + it("rejects unsetting the last_session row", async () => { + const created = await invoke("put", "/last-session", { + body: { payload: { version: 1, tabs: [] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const res = await invoke("post", "/:id/unset-default", { + params: { id: String(id) }, + }); + expect(res.statusCode).toBe(404); + }); +}); + +describe("POST /:id/apply", () => { + it("touches lastUsedAt and returns the parsed payload", async () => { + const created = await invoke("post", "/", { + body: { name: "A", payload: { version: 1, tabs: [{ slotId: "a" }] } }, + }); + const id = (created.jsonBody as unknown as { id: number }).id; + + const res = await invoke("post", "/:id/apply", { + params: { id: String(id) }, + }); + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toMatchObject({ + payload: { version: 1, tabs: [{ slotId: "a" }] }, + }); + expect( + (res.jsonBody as unknown as { lastUsedAt: string | null }).lastUsedAt, + ).toBeTruthy(); + }); +}); + +describe("PUT /last-session and GET /last-session", () => { + it("returns null before any save", async () => { + const res = await invoke("get", "/last-session"); + expect(res.jsonBody).toBeNull(); + }); + + it("upserts idempotently - two calls produce one row", async () => { + await invoke("put", "/last-session", { + body: { payload: { version: 1, tabs: [] } }, + }); + await invoke("put", "/last-session", { + body: { payload: { version: 1, tabs: [{ slotId: "a" }] } }, + }); + + const list = await invoke("get", "/"); + const lastSessionRows = ( + list.jsonBody as unknown as { kind: string }[] + ).filter((w) => w.kind === "last_session"); + expect(lastSessionRows).toHaveLength(1); + + const res = await invoke("get", "/last-session"); + expect(res.jsonBody).toMatchObject({ tabCount: 1 }); + }); +}); diff --git a/src/backend/tests/database/sync-references.test.ts b/src/backend/tests/database/sync-references.test.ts new file mode 100644 index 0000000..ac25805 --- /dev/null +++ b/src/backend/tests/database/sync-references.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { + deserializeSyncReferences, + serializeSyncReferences, +} from "../../database/routes/sync-references.js"; + +describe("sync references", () => { + it("serializes database-local host IDs as stable sync IDs", async () => { + const row = await serializeSyncReferences( + "hosts", + { + id: 7, + credentialId: 12, + rdpCredentialId: 13, + vncCredentialId: null, + telnetCredentialId: null, + vaultProfileId: 4, + }, + async (entityType, id) => `${entityType}-${id}`, + ); + + expect(row).toMatchObject({ + credentialSyncId: "sshCredentials-12", + rdpCredentialSyncId: "sshCredentials-13", + vncCredentialSyncId: null, + telnetCredentialSyncId: null, + vaultProfileSyncId: "vaultProfiles-4", + }); + expect(row).not.toHaveProperty("credentialId"); + expect(row).not.toHaveProperty("vaultProfileId"); + }); + + it("resolves stable sync IDs to IDs from the receiving database", async () => { + const ids = new Map([ + ["sshCredentials:credential-sync", 91], + ["vaultProfiles:vault-sync", 37], + ]); + const row = await deserializeSyncReferences( + "hosts", + { + credentialId: 12, + credentialSyncId: "credential-sync", + rdpCredentialSyncId: null, + vncCredentialSyncId: null, + telnetCredentialSyncId: null, + vaultProfileSyncId: "vault-sync", + }, + async (entityType, syncId) => ids.get(`${entityType}:${syncId}`) ?? null, + ); + + expect(row).toMatchObject({ + credentialId: 91, + rdpCredentialId: null, + vncCredentialId: null, + telnetCredentialId: null, + vaultProfileId: 37, + }); + expect(row).not.toHaveProperty("credentialSyncId"); + }); + + it("rejects a row whose referenced dependency has not synced", async () => { + await expect( + deserializeSyncReferences( + "sshFolders", + { credentialSyncId: "missing" }, + async () => null, + ), + ).rejects.toThrow("Missing sshCredentials dependency"); + }); +}); diff --git a/src/backend/tests/electron/backend-paths.test.ts b/src/backend/tests/electron/backend-paths.test.ts new file mode 100644 index 0000000..a0647e4 --- /dev/null +++ b/src/backend/tests/electron/backend-paths.test.ts @@ -0,0 +1,33 @@ +import { createRequire } from "node:module"; +import { describe, expect, it } from "vitest"; + +const require = createRequire(import.meta.url); +const { getUnpackedAppRoot } = + require("../../../../electron/backend-paths.cjs") as { + getUnpackedAppRoot: (appRoot: string) => string; + }; + +describe("getUnpackedAppRoot", () => { + it.each([ + [ + "/Applications/Termix.app/Contents/Resources/app.asar", + "/Applications/Termix.app/Contents/Resources/app.asar.unpacked", + ], + [ + "/Applications/Termix.app/Contents/Resources/app-arm64.asar", + "/Applications/Termix.app/Contents/Resources/app-arm64.asar.unpacked", + ], + [ + "/Applications/Termix.app/Contents/Resources/app-x64.asar", + "/Applications/Termix.app/Contents/Resources/app-x64.asar.unpacked", + ], + ])("maps %s to its matching unpacked directory", (appRoot, expected) => { + expect(getUnpackedAppRoot(appRoot)).toBe(expected); + }); + + it("does not append the suffix twice", () => { + const appRoot = + "/Applications/Termix.app/Contents/Resources/app-arm64.asar.unpacked"; + expect(getUnpackedAppRoot(appRoot)).toBe(appRoot); + }); +}); diff --git a/src/backend/tests/hosts/auth-manager.test.ts b/src/backend/tests/hosts/auth-manager.test.ts new file mode 100644 index 0000000..2edcc9d --- /dev/null +++ b/src/backend/tests/hosts/auth-manager.test.ts @@ -0,0 +1,204 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentHostResolutionRepository: () => ({ + findCredentialByIdForUser: async () => null, + }), +})); + +vi.mock("../../utils/logger.js", () => ({ + sshLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + authLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +import { SSHAuthManager } from "../../hosts/auth-manager.js"; + +function createManager() { + const sent: Record[] = []; + const ws = { send: (data: string) => sent.push(JSON.parse(data)) } as any; + const manager = new SSHAuthManager({ + userId: "user-1", + ws, + hostId: 1, + isKeyboardInteractive: false, + keyboardInteractiveResponded: false, + keyboardInteractiveFinish: null, + totpPromptSent: false, + warpgateAuthPromptSent: false, + totpTimeout: null, + warpgateAuthTimeout: null, + totpAttempts: 0, + }); + return { manager, sent }; +} + +describe("SSHAuthManager.handleKeyboardInteractive", () => { + beforeEach(() => { + vi.useRealTimers(); + }); + + it("routes a TOTP verification prompt to the totp flow", () => { + const { manager, sent } = createManager(); + const finish = vi.fn(); + + manager.handleKeyboardInteractive( + "", + "", + "", + [{ prompt: "Verification code: ", echo: true }], + finish, + { username: "root", authType: "none" }, + ); + + expect(sent).toEqual([ + { + type: "connection_log", + data: { + stage: "auth", + level: "info", + message: "TOTP verification required", + }, + }, + { type: "totp_required", prompt: "Verification code: " }, + ]); + expect(finish).not.toHaveBeenCalled(); + }); + + it("forwards echo:true for a JumpCloud-style push/TOTP menu prompt", () => { + const { manager, sent } = createManager(); + const finish = vi.fn(); + + manager.handleKeyboardInteractive( + "", + "", + "", + [{ prompt: "Choose [1] Push, or [2] TOTP: ", echo: true }], + finish, + { username: "root", authType: "none" }, + ); + + expect(sent).toEqual([ + { + type: "connection_log", + data: { + stage: "auth", + level: "info", + message: "Password authentication required", + }, + }, + { + type: "password_required", + prompt: "Choose [1] Push, or [2] TOTP: ", + echo: true, + }, + ]); + }); + + it("silently auto-answers a plain password prompt when a stored password exists", () => { + const { manager, sent } = createManager(); + const finish = vi.fn(); + + manager.handleKeyboardInteractive( + "", + "", + "", + [{ prompt: "Password: ", echo: false }], + finish, + { username: "root", password: "hunter2", authType: "password" }, + ); + + expect(finish).toHaveBeenCalledWith(["hunter2"]); + expect(sent).toEqual([]); + }); + + it("prompts the user for a push-confirm prompt and accepts an empty response", () => { + const { manager, sent } = createManager(); + const finish = vi.fn(); + + manager.handleKeyboardInteractive( + "", + "", + "", + [{ prompt: "Press enter to send Push request: ", echo: true }], + finish, + { username: "root", authType: "none" }, + ); + + expect(sent).toEqual([ + { + type: "connection_log", + data: { + stage: "auth", + level: "info", + message: "Password authentication required", + }, + }, + { + type: "password_required", + prompt: "Press enter to send Push request: ", + echo: true, + }, + ]); + + manager.context.keyboardInteractiveFinish?.([""]); + + expect(finish).toHaveBeenCalledWith([""]); + }); + + it("uses a longer timeout for push-style prompts than generic prompts", () => { + vi.useFakeTimers(); + try { + const { manager, sent } = createManager(); + const finish = vi.fn(); + + manager.handleKeyboardInteractive( + "", + "", + "", + [{ prompt: "Press enter to send Push request: ", echo: true }], + finish, + { username: "root", authType: "none" }, + ); + + vi.advanceTimersByTime(180001); + expect(sent.some((m) => m.type === "error")).toBe(false); + + vi.advanceTimersByTime(120000); + expect(sent.some((m) => m.type === "error")).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + + it("routes Warpgate prompts to the warpgate flow, not the generic path", () => { + const { manager, sent } = createManager(); + const finish = vi.fn(); + + manager.handleKeyboardInteractive( + "Warpgate Authentication", + "Visit https://warpgate.example.com/auth to continue. Security key: AB12", + "", + [{ prompt: "Press enter once done: ", echo: true }], + finish, + { username: "root", authType: "none" }, + ); + + expect(sent).toEqual([ + { + type: "connection_log", + data: { + stage: "auth", + level: "info", + message: "Warpgate authentication required", + }, + }, + { + type: "warpgate_auth_required", + url: "https://warpgate.example.com/auth", + securityKey: "AB12", + instructions: + "Visit https://warpgate.example.com/auth to continue. Security key: AB12", + }, + ]); + }); +}); diff --git a/src/backend/tests/hosts/credential-username.test.ts b/src/backend/tests/hosts/credential-username.test.ts index daf76cf..68fc4cf 100644 --- a/src/backend/tests/hosts/credential-username.test.ts +++ b/src/backend/tests/hosts/credential-username.test.ts @@ -101,4 +101,62 @@ describe("expandOidcUsername", () => { "$oidc.preferred_username", ); }); + + it("strips the ldap:{providerId}: prefix for genuine LDAP users", async () => { + vi.doMock("../../database/repositories/factory.js", () => ({ + createCurrentUserRepository: () => ({ + findById: async () => ({ + oidcIdentifier: "ldap:1:jdoe", + ssoProviderId: 1, + }), + }), + createCurrentSsoProviderRepository: () => ({ + findById: async () => ({ type: "ldap" }), + }), + })); + + const { expandOidcUsername: expand } = + await import("../../hosts/credential-username.js"); + expect(await expand("$oidc.preferred_username", "user-1")).toBe("jdoe"); + }); + + it("does not strip a spoofed ldap: identifier from a non-LDAP provider", async () => { + vi.doMock("../../database/repositories/factory.js", () => ({ + createCurrentUserRepository: () => ({ + findById: async () => ({ + oidcIdentifier: "ldap:1:admin", + ssoProviderId: 1, + }), + }), + createCurrentSsoProviderRepository: () => ({ + findById: async () => ({ type: "oidc" }), + }), + })); + + const { expandOidcUsername: expand } = + await import("../../hosts/credential-username.js"); + expect(await expand("$oidc.preferred_username", "user-1")).toBe( + "ldap:1:admin", + ); + }); + + it("does not strip when the embedded provider id is not the user's provider", async () => { + vi.doMock("../../database/repositories/factory.js", () => ({ + createCurrentUserRepository: () => ({ + findById: async () => ({ + oidcIdentifier: "ldap:1:admin", + ssoProviderId: 5, + }), + }), + createCurrentSsoProviderRepository: () => ({ + findById: async () => ({ type: "ldap" }), + }), + })); + + const { expandOidcUsername: expand } = + await import("../../hosts/credential-username.js"); + expect(await expand("$oidc.preferred_username", "user-1")).toBe( + "ldap:1:admin", + ); + }); }); diff --git a/src/backend/tests/hosts/file-manager/ca-cert-auth.test.ts b/src/backend/tests/hosts/file-manager/ca-cert-auth.test.ts new file mode 100644 index 0000000..d5cc803 --- /dev/null +++ b/src/backend/tests/hosts/file-manager/ca-cert-auth.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const setupCACertAuth = vi.fn(); +const warn = vi.fn(); + +vi.mock("../../../hosts/opkssh-cert-auth.js", () => ({ + setupCACertAuth: (...args: unknown[]) => setupCACertAuth(...args), +})); + +vi.mock("../../../utils/logger.js", () => ({ + fileLogger: { warn, info: vi.fn(), error: vi.fn(), debug: vi.fn() }, +})); + +const { applyCACertIfPresent } = + await import("../../../hosts/file-manager/ca-cert-auth.js"); + +/** + * `setupCACertAuth` had no call site in the file manager at all, while its + * sibling `setupOPKSSHCertAuth` had two. So a host whose key is paired with a + * user-managed CA-signed certificate authenticated in the terminal and failed + * over SFTP, and OPKSSH certificates โ€” going through the other helper โ€” worked + * in both places. + */ +describe("applyCACertIfPresent", () => { + const client = {} as never; + const key = Buffer.from("private-key"); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("attaches a certificate the host carries", async () => { + const config: Record = {}; + + await applyCACertIfPresent( + config, + client, + key, + { certPublicKey: "ssh-rsa-cert-v01@openssh.com AAAA" }, + "root", + "passphrase", + ); + + expect(setupCACertAuth).toHaveBeenCalledWith( + config, + client, + key, + "ssh-rsa-cert-v01@openssh.com AAAA", + "root", + "passphrase", + ); + }); + + it("does nothing when there is no certificate", async () => { + // Most hosts. Touching the connection here would change key-only auth. + for (const certPublicKey of [undefined, null, "", " "]) { + await applyCACertIfPresent( + {}, + client, + key, + { certPublicKey }, + "root", + undefined, + ); + } + + expect(setupCACertAuth).not.toHaveBeenCalled(); + }); + + it("leaves the connection usable when the certificate is unusable", async () => { + // The private key on its own may still be accepted โ€” which is what + // happened before this was wired up. Failing the connection here would + // turn a working setup into a broken one. + setupCACertAuth.mockRejectedValueOnce(new Error("bad cert format")); + + await expect( + applyCACertIfPresent( + {}, + client, + key, + { certPublicKey: "garbage" }, + "root", + undefined, + ), + ).resolves.toBeUndefined(); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("CA certificate setup failed"), + expect.objectContaining({ operation: "sftp_ca_cert_auth_failed" }), + ); + }); +}); diff --git a/src/backend/tests/hosts/file-manager/direct-transfer-routing.test.ts b/src/backend/tests/hosts/file-manager/direct-transfer-routing.test.ts new file mode 100644 index 0000000..321b991 --- /dev/null +++ b/src/backend/tests/hosts/file-manager/direct-transfer-routing.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { + buildDirectProbeCommand, + buildDirectRsyncCommand, + quoteShell, + shouldBenchmarkDirectTransfer, + shouldUseDirectTransfer, +} from "../../../hosts/file-manager/direct-transfer-routing.js"; + +const endpoint = { host: "10.0.0.2", port: 2222, username: "deploy" }; + +describe("direct transfer routing", () => { + it("keeps small transfers on the relay without benchmarking", () => { + expect(shouldBenchmarkDirectTransfer(32 * 1024 * 1024 - 1)).toBe(false); + expect(shouldBenchmarkDirectTransfer(32 * 1024 * 1024)).toBe(true); + }); + + it("requires a meaningful speed advantage", () => { + expect(shouldUseDirectTransfer(700, 1000)).toBe(true); + expect(shouldUseDirectTransfer(850, 1000)).toBe(false); + expect(shouldUseDirectTransfer(0, 1000)).toBe(false); + }); + + it("probes without accepting passwords or unknown host keys", () => { + const command = buildDirectProbeCommand(endpoint); + expect(command).toContain("BatchMode=yes"); + expect(command).toContain("StrictHostKeyChecking=yes"); + expect(command).toContain("ConnectTimeout=5"); + expect(command).toContain("-p 2222"); + }); + + it("quotes source and destination paths for rsync", () => { + const command = buildDirectRsyncCommand( + endpoint, + ["/srv/a file", "/srv/it's-safe"], + "/opt/releases", + true, + ); + expect(command).toContain("--partial --append-verify"); + expect(command).toContain("--protect-args"); + expect(command).toContain(quoteShell("/srv/a file")); + expect(command).toContain(quoteShell("/srv/it's-safe")); + expect(command).toContain("deploy@10.0.0.2:/opt/releases/"); + }); + + it("brackets IPv6 destinations", () => { + const command = buildDirectProbeCommand({ + host: "2001:db8::2", + port: 22, + username: "root", + }); + expect(command).toContain("root@[2001:db8::2]"); + }); +}); diff --git a/src/backend/tests/hosts/file-manager/transfer-integrity.test.ts b/src/backend/tests/hosts/file-manager/transfer-integrity.test.ts new file mode 100644 index 0000000..9d883fb --- /dev/null +++ b/src/backend/tests/hosts/file-manager/transfer-integrity.test.ts @@ -0,0 +1,68 @@ +import { Readable } from "node:stream"; +import { describe, expect, it, vi } from "vitest"; +import { + hashSftpFile, + verifySftpFileIntegrity, +} from "../../../hosts/file-manager/transfer-integrity.js"; + +type SFTPWrapper = import("ssh2").SFTPWrapper; + +function fakeSftp(contents: Record): SFTPWrapper { + return { + createReadStream: vi.fn((path: string) => { + const value = contents[path]; + if (value === undefined) { + return new Readable({ + read() { + this.destroy(new Error(`Missing file: ${path}`)); + }, + }); + } + return Readable.from(Array.isArray(value) ? value : [value]); + }), + } as unknown as SFTPWrapper; +} + +describe("transfer integrity", () => { + it("hashes all chunks in an SFTP file", async () => { + const sftp = fakeSftp({ + "/file": [Buffer.from("hello "), Buffer.from("world")], + }); + + await expect(hashSftpFile(sftp, "/file")).resolves.toBe( + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + ); + }); + + it("accepts matching source and destination files", async () => { + const source = fakeSftp({ "/source": Buffer.from("same bytes") }); + const dest = fakeSftp({ "/dest": Buffer.from("same bytes") }); + + await expect( + verifySftpFileIntegrity(source, dest, "/source", "/dest"), + ).resolves.toMatchObject({ algorithm: "sha256" }); + }); + + it("rejects a corrupted destination", async () => { + const source = fakeSftp({ "/source": Buffer.from("expected") }); + const dest = fakeSftp({ "/dest": Buffer.from("corrupted") }); + + await expect( + verifySftpFileIntegrity(source, dest, "/source", "/dest"), + ).rejects.toThrow("SHA-256 verification failed for /source"); + }); + + it("aborts hashing with the caller's cancellation error", async () => { + const sftp = fakeSftp({ "/file": Buffer.from("data") }); + const cancelled = new Error("cancelled by test"); + + await expect( + hashSftpFile( + sftp, + "/file", + () => true, + () => cancelled, + ), + ).rejects.toBe(cancelled); + }); +}); diff --git a/src/backend/tests/hosts/file-manager/transfer-routing.test.ts b/src/backend/tests/hosts/file-manager/transfer-routing.test.ts new file mode 100644 index 0000000..cd3fa93 --- /dev/null +++ b/src/backend/tests/hosts/file-manager/transfer-routing.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { + estimateIncompressibleSample, + resolveArchiveTransferMethod, + type TransferScanSummary, +} from "../../../hosts/file-manager/transfer-routing.js"; + +describe("transfer content sampling", () => { + it("recognizes repetitive content as compressible", () => { + expect(estimateIncompressibleSample(Buffer.alloc(64 * 1024, 65))).toBe( + false, + ); + }); + + it("recognizes high-entropy content as incompressible", () => { + const sample = Buffer.alloc(64 * 1024); + let state = 0x12345678; + for (let i = 0; i < sample.length; i++) { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + sample[i] = state & 0xff; + } + expect(estimateIncompressibleSample(sample)).toBe(true); + }); + + it("prefers sampled content over a misleading extension", () => { + const summary: TransferScanSummary = { + fileCount: 120, + totalBytes: 1024 * 1024 * 1024, + largestFileBytes: 32 * 1024 * 1024, + incompressibleRatio: 0, + sampledIncompressibleRatio: 1, + }; + expect( + resolveArchiveTransferMethod("auto", summary, "unix", "unix", true, true), + ).toBe("item_sftp"); + }); +}); diff --git a/src/backend/tests/hosts/file-manager/transfer-tuning.test.ts b/src/backend/tests/hosts/file-manager/transfer-tuning.test.ts new file mode 100644 index 0000000..630b154 --- /dev/null +++ b/src/backend/tests/hosts/file-manager/transfer-tuning.test.ts @@ -0,0 +1,127 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { promises as fs } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { + clearTransferProfiles, + flushTransferProfiles, + getRecentDirectRouteDecision, + getTransferProfile, + initializeTransferProfiles, + recordDirectRouteBenchmark, + recordDirectRouteOutcome, + recordTransferProfile, + selectTransferTuning, + updateTransferProfile, +} from "../../../hosts/file-manager/transfer-tuning.js"; + +const MB = 1024 * 1024; + +describe("adaptive transfer tuning", () => { + beforeEach(clearTransferProfiles); + + it("keeps small files on one conservative lane", () => { + expect(selectTransferTuning(8 * MB)).toEqual({ + parallelSegmentCount: 1, + pipelineConcurrency: 8, + }); + }); + + it("uses more lanes for large transfers without exceeding segment count", () => { + expect(selectTransferTuning(2 * 1024 * MB)).toEqual({ + parallelSegmentCount: 4, + pipelineConcurrency: 32, + }); + expect(selectTransferTuning(300 * MB).parallelSegmentCount).toBe(2); + }); + + it("honours an explicit lane selection", () => { + expect(selectTransferTuning(2 * 1024 * MB, undefined, 3)).toMatchObject({ + parallelSegmentCount: 3, + }); + }); + + it("backs off after failures", () => { + const profile = updateTransferProfile(undefined, { + bytes: 256 * MB, + durationMs: 1000, + lanes: 4, + pipelineConcurrency: 32, + failed: true, + now: 1, + }); + expect(profile.preferredLanes).toBe(2); + expect(profile.pipelineConcurrency).toBe(16); + }); + + it("learns and expires host-pair profiles", () => { + recordTransferProfile("a->b", { + bytes: 256 * MB, + durationMs: 2000, + lanes: 2, + pipelineConcurrency: 32, + failed: false, + now: 100, + }); + expect(getTransferProfile("a->b", 101)?.samples).toBe(1); + expect(getTransferProfile("a->b", 8 * 24 * 60 * 60 * 1000)).toBeUndefined(); + }); + + it("reuses recent route benchmarks and cools down failed direct paths", () => { + recordDirectRouteBenchmark("a->b", 700, 1000, 100); + expect(getRecentDirectRouteDecision("a->b", 1_000, 101)).toEqual({ + useDirect: true, + directMs: 700, + relayMs: 1000, + }); + + recordDirectRouteOutcome("a->b", true, 102, 500); + expect(getRecentDirectRouteDecision("a->b", 1_000, 103)?.useDirect).toBe( + false, + ); + expect(getRecentDirectRouteDecision("a->b", 1_000, 1_103)).toBeUndefined(); + }); + + it("persists only anonymous local profiles across restarts", async () => { + const previousDataDir = process.env.DATA_DIR; + const dataDir = await fs.mkdtemp(path.join(tmpdir(), "termix-transfer-")); + process.env.DATA_DIR = dataDir; + const rawKey = "root@10.0.0.1->deploy@10.0.0.2"; + const now = Date.now(); + + try { + await initializeTransferProfiles(); + recordTransferProfile(rawKey, { + bytes: 256 * MB, + durationMs: 2000, + lanes: 2, + pipelineConcurrency: 16, + failed: false, + now, + }); + recordDirectRouteBenchmark(rawKey, 700, 1000, now); + await flushTransferProfiles(); + + const stored = await fs.readFile( + path.join(dataDir, "adaptive-transfer-profiles.json"), + "utf8", + ); + expect(stored).not.toContain(rawKey); + expect(Object.keys(JSON.parse(stored).profiles)[0]).toMatch( + /^[a-f0-9]{64}$/, + ); + + clearTransferProfiles(); + await initializeTransferProfiles(); + expect(getTransferProfile(rawKey, now + 1)).toMatchObject({ samples: 1 }); + expect( + getRecentDirectRouteDecision(rawKey, 1_000, now + 1), + ).toMatchObject({ useDirect: true }); + } finally { + clearTransferProfiles(); + if (previousDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = previousDataDir; + await fs.rm(dataDir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/backend/tests/hosts/file-manager/trash-service.test.ts b/src/backend/tests/hosts/file-manager/trash-service.test.ts new file mode 100644 index 0000000..9669b75 --- /dev/null +++ b/src/backend/tests/hosts/file-manager/trash-service.test.ts @@ -0,0 +1,235 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { SFTPWrapper } from "ssh2"; +import { afterEach, describe, expect, it } from "vitest"; +import { + isSafeTrashSource, + listTrash, + moveToTrash, + permanentlyDeleteTrashItem, + restoreTrashItem, +} from "../../../hosts/file-manager/trash-service.js"; + +const temporaryDirectories: string[] = []; + +// Windows only allows symlink creation with elevation or Developer Mode, so the +// symlink safety test is skipped where the OS refuses to create one at all. +const canCreateSymlinks = (() => { + const probe = fs.mkdtempSync(path.join(os.tmpdir(), "termix-symlink-probe-")); + try { + fs.mkdirSync(path.join(probe, "target")); + fs.symlinkSync(path.join(probe, "target"), path.join(probe, "link"), "dir"); + return true; + } catch { + return false; + } finally { + fs.rmSync(probe, { recursive: true, force: true }); + } +})(); + +function localSftp(home: string): SFTPWrapper { + const callback = ( + promise: Promise, + done: (error: Error | undefined, value?: T) => void, + ) => + promise + .then((value) => done(undefined, value)) + .catch((error) => done(error)); + return { + realpath( + _target: string, + done: (error: Error | undefined, value?: string) => void, + ) { + done(undefined, home); + }, + stat( + target: string, + done: (error: Error | undefined, value?: fs.Stats) => void, + ) { + callback(fs.promises.stat(target), done); + }, + lstat( + target: string, + done: (error: Error | undefined, value?: fs.Stats) => void, + ) { + callback(fs.promises.lstat(target), done); + }, + readdir( + target: string, + done: (error: Error | undefined, value?: unknown[]) => void, + ) { + callback( + fs.promises.readdir(target, { withFileTypes: true }).then((entries) => + entries.map((entry) => ({ + filename: entry.name, + longname: entry.name, + attrs: {}, + })), + ), + done, + ); + }, + readFile( + target: string, + done: (error: Error | undefined, value?: Buffer) => void, + ) { + callback(fs.promises.readFile(target), done); + }, + writeFile(target: string, data: string, done: (error?: Error) => void) { + fs.promises + .writeFile(target, data) + .then(() => done()) + .catch(done); + }, + rename(from: string, to: string, done: (error?: Error) => void) { + fs.promises + .rename(from, to) + .then(() => done()) + .catch(done); + }, + unlink(target: string, done: (error?: Error) => void) { + fs.promises + .unlink(target) + .then(() => done()) + .catch(done); + }, + mkdir(target: string, done: (error?: Error) => void) { + fs.promises + .mkdir(target) + .then(() => done()) + .catch(done); + }, + rmdir(target: string, done: (error?: Error) => void) { + fs.promises + .rmdir(target) + .then(() => done()) + .catch(done); + }, + } as unknown as SFTPWrapper; +} + +async function fixture() { + const home = await fs.promises.mkdtemp( + path.join(os.tmpdir(), "termix-trash-"), + ); + temporaryDirectories.push(home); + return { home, sftp: localSftp(home) }; +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => + fs.promises.rm(directory, { recursive: true, force: true }), + ), + ); +}); + +describe("file manager trash safety", () => { + it("rejects roots and paths inside the trash", () => { + expect(isSafeTrashSource("/", "/home/user/.termix-trash")).toBe(false); + expect(isSafeTrashSource("C:/", "C:/Users/user/.termix-trash")).toBe(false); + expect( + isSafeTrashSource( + "/home/user/.termix-trash/files/a", + "/home/user/.termix-trash", + ), + ).toBe(false); + }); + + it("accepts ordinary files and directories", () => { + expect( + isSafeTrashSource("/home/user/report.txt", "/home/user/.termix-trash"), + ).toBe(true); + }); + + it("moves, lists, and restores a file without changing its contents", async () => { + const { home, sftp } = await fixture(); + const original = path.join(home, "report.txt"); + await fs.promises.writeFile(original, "important"); + + const trashed = await moveToTrash(sftp, original); + expect(fs.existsSync(original)).toBe(false); + expect(await listTrash(sftp, 7)).toEqual([trashed]); + + await restoreTrashItem(sftp, trashed.id); + expect(await fs.promises.readFile(original, "utf8")).toBe("important"); + expect(await listTrash(sftp, 7)).toEqual([]); + }); + + it("permanently deletes only the stored trash path", async () => { + const { home, sftp } = await fixture(); + const original = path.join(home, "folder"); + await fs.promises.mkdir(original); + await fs.promises.writeFile(path.join(original, "nested.txt"), "data"); + const trashed = await moveToTrash(sftp, original); + + await permanentlyDeleteTrashItem(sftp, trashed.id); + expect(await listTrash(sftp, 7)).toEqual([]); + }); + + it.skipIf(!canCreateSymlinks)( + "does not follow directory symlinks during permanent deletion", + async () => { + const { home, sftp } = await fixture(); + const target = path.join(home, "target"); + const link = path.join(home, "link"); + await fs.promises.mkdir(target); + await fs.promises.writeFile(path.join(target, "keep.txt"), "keep"); + await fs.promises.symlink(target, link, "dir"); + + const trashed = await moveToTrash(sftp, link); + await permanentlyDeleteTrashItem(sftp, trashed.id); + + expect( + await fs.promises.readFile(path.join(target, "keep.txt"), "utf8"), + ).toBe("keep"); + }, + ); + + it("refuses tampered metadata instead of deleting an arbitrary path", async () => { + const { home, sftp } = await fixture(); + const original = path.join(home, "discard.txt"); + const protectedFile = path.join(home, "keep.txt"); + await fs.promises.writeFile(original, "discard"); + await fs.promises.writeFile(protectedFile, "keep"); + const trashed = await moveToTrash(sftp, original); + const metadata = path.join( + home, + ".termix-trash", + "info", + `${trashed.id}.json`, + ); + const data = JSON.parse(await fs.promises.readFile(metadata, "utf8")); + data.trashPath = protectedFile; + await fs.promises.writeFile(metadata, JSON.stringify(data)); + + await expect(permanentlyDeleteTrashItem(sftp, trashed.id)).rejects.toThrow( + "Invalid trash metadata", + ); + expect(await fs.promises.readFile(protectedFile, "utf8")).toBe("keep"); + }); + + it("prunes items after the configured retention period", async () => { + const { home, sftp } = await fixture(); + const original = path.join(home, "old.txt"); + await fs.promises.writeFile(original, "old"); + const trashed = await moveToTrash(sftp, original); + const metadata = path.join( + home, + ".termix-trash", + "info", + `${trashed.id}.json`, + ); + const data = JSON.parse(await fs.promises.readFile(metadata, "utf8")); + data.deletedAt = "2020-01-01T00:00:00.000Z"; + await fs.promises.writeFile(metadata, JSON.stringify(data)); + + expect(await listTrash(sftp, 7)).toEqual([]); + expect( + fs.existsSync(path.join(home, ".termix-trash", "files", trashed.id)), + ).toBe(false); + }); +}); diff --git a/src/backend/tests/hosts/guacamole/jump-tunnel-endpoint.test.ts b/src/backend/tests/hosts/guacamole/jump-tunnel-endpoint.test.ts new file mode 100644 index 0000000..cca9f1c --- /dev/null +++ b/src/backend/tests/hosts/guacamole/jump-tunnel-endpoint.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { resolveJumpTunnelEndpoint } from "../../../hosts/guacamole/jump-tunnel-endpoint.js"; + +describe("resolveJumpTunnelEndpoint", () => { + it("keeps an in-process guacd tunnel on loopback", () => { + expect(resolveJumpTunnelEndpoint("localhost")).toEqual({ + bindHost: "127.0.0.1", + advertisedHost: "127.0.0.1", + }); + }); + + it("exposes the tunnel to a separate guacd container", () => { + expect(resolveJumpTunnelEndpoint("guacd")).toEqual({ + bindHost: "0.0.0.0", + advertisedHost: "termix", + }); + }); + + it("supports a custom backend hostname for external guacd", () => { + expect( + resolveJumpTunnelEndpoint("guacd.example", "termix-backend"), + ).toEqual({ + bindHost: "0.0.0.0", + advertisedHost: "termix-backend", + }); + }); +}); diff --git a/src/backend/tests/hosts/guacamole/rdp-settings.test.ts b/src/backend/tests/hosts/guacamole/rdp-settings.test.ts new file mode 100644 index 0000000..193c041 --- /dev/null +++ b/src/backend/tests/hosts/guacamole/rdp-settings.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { + buildRdpSettings, + resolveRdpDomain, +} from "../../../hosts/guacamole/rdp-settings.js"; + +describe("buildRdpSettings", () => { + it("keeps saved RDP settings authoritative over stale advanced config", () => { + expect( + buildRdpSettings({ + port: 3390, + domain: "EXAMPLE", + security: "nla", + ignoreCert: true, + guacConfig: { + port: 3389, + domain: "OLD", + security: "rdp", + "ignore-cert": false, + "color-depth": 24, + }, + guacdOverrides: { guacdHost: "guacd.internal" }, + }), + ).toEqual({ + port: 3390, + domain: "EXAMPLE", + security: "nla", + "ignore-cert": true, + "color-depth": 24, + guacdHost: "guacd.internal", + }); + }); + + it("preserves an advanced security value when no saved value exists", () => { + expect( + buildRdpSettings({ + port: 3389, + ignoreCert: false, + guacConfig: { security: "tls" }, + guacdOverrides: {}, + }).security, + ).toBe("tls"); + }); + + it("uses the prompted domain for prompt-on-connect authentication", () => { + expect(resolveRdpDomain("none", "EXAMPLE", "OLD")).toBe("EXAMPLE"); + expect(resolveRdpDomain("none", "", "OLD")).toBe(""); + }); + + it("keeps the stored domain for saved authentication", () => { + expect(resolveRdpDomain("direct", "EXAMPLE", "SAVED")).toBe("SAVED"); + expect(resolveRdpDomain("none", undefined, "SAVED")).toBe("SAVED"); + }); +}); diff --git a/src/backend/tests/hosts/guacamole/recording-settings.test.ts b/src/backend/tests/hosts/guacamole/recording-settings.test.ts new file mode 100644 index 0000000..68b19c5 --- /dev/null +++ b/src/backend/tests/hosts/guacamole/recording-settings.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import { withRecordingSettings } from "../../../hosts/guacamole/recording-settings.js"; + +const PATH = "/app/data/session_recordings/guacamole"; +const NAME = "b7e6c0f2-0000-4000-8000-000000000000.guac"; + +describe("withRecordingSettings", () => { + it("takes ownership of the location and filename", () => { + const merged = withRecordingSettings( + { + "recording-path": "/var/lib/termix/recordings", + "recording-name": "${GUAC_USERNAME}-${GUAC_DATE}", + "create-recording-path": false, + }, + PATH, + NAME, + ); + + expect(merged).toMatchObject({ + "recording-path": PATH, + "recording-name": NAME, + "create-recording-path": true, + }); + }); + + it("defaults the content flags when the host has no opinion", () => { + expect(withRecordingSettings({}, PATH, NAME)).toMatchObject({ + "recording-exclude-output": false, + "recording-include-keys": true, + }); + }); + + it("keeps the host's content flags, including the falsy ones", () => { + const merged = withRecordingSettings( + { + "recording-exclude-output": true, + "recording-include-keys": false, + }, + PATH, + NAME, + ); + + expect(merged).toMatchObject({ + "recording-exclude-output": true, + "recording-include-keys": false, + }); + }); + + it("leaves unrelated settings alone", () => { + const merged = withRecordingSettings( + { "recording-exclude-mouse": true, width: "1920" }, + PATH, + NAME, + ); + + expect(merged).toMatchObject({ + "recording-exclude-mouse": true, + width: "1920", + }); + }); + + it("does not mutate the settings it was given", () => { + const original = { "recording-path": "/tmp/mine" }; + withRecordingSettings(original, PATH, NAME); + + expect(original).toEqual({ "recording-path": "/tmp/mine" }); + }); +}); diff --git a/src/backend/tests/hosts/guacamole/token-service.test.ts b/src/backend/tests/hosts/guacamole/token-service.test.ts index de3069a..d6cfc99 100644 --- a/src/backend/tests/hosts/guacamole/token-service.test.ts +++ b/src/backend/tests/hosts/guacamole/token-service.test.ts @@ -65,4 +65,41 @@ describe("GuacamoleTokenService", () => { expect(tokenService.decryptToken(token)?.recording).toEqual(recording); }); + + it("preserves termixMeta through the encrypt/decrypt round trip", () => { + const termixMeta = { + termixConnectId: "connect-1", + hostId: 7, + ownerUserId: "user-1", + protocol: "rdp" as const, + }; + const token = tokenService.createRdpToken( + "windows.example.test", + "Administrator", + "secret", + {}, + undefined, + termixMeta, + ); + + expect(tokenService.decryptToken(token)?.termixMeta).toEqual(termixMeta); + }); + + it("createJoinToken sets connection.join, not connection.type", () => { + const token = tokenService.createJoinToken("guacd-conn-123", true); + const decrypted = tokenService.decryptToken(token); + + expect(decrypted?.connection.join).toBe("guacd-conn-123"); + expect(decrypted?.connection.type).toBeUndefined(); + expect(decrypted?.connection.readOnly).toBe(true); + }); + + it("createJoinToken round-trips a read-write join through decryptToken", () => { + const token = tokenService.createJoinToken("guacd-conn-456", false); + const decrypted = tokenService.decryptToken(token); + + expect(decrypted?.connection.join).toBe("guacd-conn-456"); + expect(decrypted?.connection.readOnly).toBe(false); + expect(decrypted?.recording).toBeUndefined(); + }); }); diff --git a/src/backend/tests/hosts/host-resolver-sync-id.test.ts b/src/backend/tests/hosts/host-resolver-sync-id.test.ts new file mode 100644 index 0000000..9f0eeee --- /dev/null +++ b/src/backend/tests/hosts/host-resolver-sync-id.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const findHostIdBySyncId = vi.fn(); +const canAccessHost = vi.fn(); +const findHostOwnerId = vi.fn(); +const findHostById = vi.fn(); + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentHostResolutionRepository: () => ({ + findHostIdBySyncId, + findHostOwnerId, + findHostById, + }), + createCurrentVaultProfileRepository: () => ({}), + createCurrentUserRepository: () => ({ findById: vi.fn() }), +})); + +vi.mock("../../utils/permission-manager.js", () => ({ + PermissionManager: { getInstance: () => ({ canAccessHost }) }, +})); + +vi.mock("../../utils/audit-logger.js", () => ({ logAudit: vi.fn() })); +vi.mock("../../utils/shared-host-auth-resolver.js", () => ({ + resolveRecipientSharedHostAuthentication: vi.fn(), +})); + +/** + * A numeric host id belongs to whichever database produced it. Resolving the + * desktop app's id against a sync server's table lands on whatever host owns + * that number there โ€” a different machine, with its own address, credentials + * and host key. `sync_id` is the same string on both sides. + */ +describe("resolveHostBySyncId", () => { + beforeEach(() => { + vi.clearAllMocks(); + canAccessHost.mockResolvedValue({ hasAccess: true }); + findHostOwnerId.mockResolvedValue("user-1"); + }); + + it("resolves the row carrying that sync id, whatever its local id is", async () => { + // The client's row is id 3 locally; here the same host is id 41. + findHostIdBySyncId.mockResolvedValue(41); + findHostById.mockResolvedValue({ + id: 41, + ip: "10.0.0.7", + userId: "user-1", + }); + + const { resolveHostBySyncId } = + await import("../../hosts/host-resolver.js"); + const host = await resolveHostBySyncId("sync-abc", "user-1"); + + expect(findHostIdBySyncId).toHaveBeenCalledWith("sync-abc"); + expect(findHostById).toHaveBeenCalledWith(41, "user-1"); + expect(host?.ip).toBe("10.0.0.7"); + }); + + it("returns null for a sync id this server does not have", async () => { + // Falling back to the numeric id here is what picked the wrong machine. + findHostIdBySyncId.mockResolvedValue(null); + + const { resolveHostBySyncId } = + await import("../../hosts/host-resolver.js"); + + await expect(resolveHostBySyncId("sync-unknown", "user-1")).resolves.toBe( + null, + ); + expect(findHostById).not.toHaveBeenCalled(); + }); + + it("still refuses a host the caller may not reach", async () => { + // The lookup is unscoped so shared hosts resolve; permission is decided by + // the id-based path, which must not be bypassed. + findHostIdBySyncId.mockResolvedValue(41); + canAccessHost.mockResolvedValue({ hasAccess: false }); + + const { resolveHostBySyncId } = + await import("../../hosts/host-resolver.js"); + + await expect(resolveHostBySyncId("sync-abc", "intruder")).resolves.toBe( + null, + ); + expect(findHostById).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/hosts/host-resolver.test.ts b/src/backend/tests/hosts/host-resolver.test.ts index 9729fd9..f068e15 100644 --- a/src/backend/tests/hosts/host-resolver.test.ts +++ b/src/backend/tests/hosts/host-resolver.test.ts @@ -6,20 +6,25 @@ const state = vi.hoisted(() => ({ isAdminBypass: false, overrideCredentialId: null as number | null, credentials: new Map>(), - sharedSecret: null as Record | null, + vaultProfile: null as Record | null, auditCalls: [] as Record[], + folderCredentialId: null as number | null, + sharedSecret: null as Record | null, })); vi.mock("../../database/repositories/factory.js", () => ({ createCurrentHostResolutionRepository: () => ({ findHostOwnerId: async () => (state.host?.userId as string) ?? null, findHostById: async () => (state.host ? { ...state.host } : null), - findOverrideCredentialId: async () => state.overrideCredentialId, findCredentialByIdForUser: async (credentialId: number, userId: string) => state.credentials.get(`${credentialId}:${userId}`) ?? null, + findFolderCredentialId: async () => state.folderCredentialId, + }), + createCurrentSharedHostAuthOverrideRepository: () => ({ + findCredentialId: async () => state.overrideCredentialId, }), createCurrentVaultProfileRepository: () => ({ - findById: async () => null, + findById: async () => state.vaultProfile, }), createCurrentUserRepository: () => ({ findById: async (userId: string) => ({ id: userId, username: userId }), @@ -77,6 +82,7 @@ function baseHost(overrides: Record = {}) { keyPassword: null, keyType: null, credentialId: null, + shareSshAuth: false, vaultProfileId: null, sudoPassword: "owner-sudo", autostartPassword: "auto-pass", @@ -99,8 +105,10 @@ beforeEach(() => { state.isAdminBypass = false; state.overrideCredentialId = null; state.credentials.clear(); - state.sharedSecret = null; + state.vaultProfile = null; state.auditCalls = []; + state.folderCredentialId = null; + state.sharedSecret = null; }); describe("resolveHostById", () => { @@ -138,8 +146,69 @@ describe("resolveHostById", () => { expect(host.sudoPassword).toBe("owner-sudo"); }); - it("uses the share snapshot for a non-owner and strips owner-only secrets", async () => { - state.host = baseHost({ username: "" }); + it("falls back to the host's folder-assigned credential when none is set on the host", async () => { + state.host = baseHost({ + authType: "credential", + credentialId: null, + folder: "switches", + username: "", + password: null, + }); + state.folderCredentialId = 11; + state.credentials.set("11:owner", { + id: 11, + username: "folder-user", + authType: "password", + password: "folder-pass", + privateKey: null, + key: null, + keyPassword: null, + keyType: null, + }); + + const host = (await resolveHostById(42, "owner")) as Record< + string, + unknown + >; + expect(host.password).toBe("folder-pass"); + expect(host.username).toBe("folder-user"); + expect(host.authType).toBe("password"); + }); + + it("prefers the host's own credential over its folder's credential", async () => { + state.host = baseHost({ + authType: "credential", + credentialId: 9, + folder: "switches", + username: "", + password: null, + }); + state.folderCredentialId = 11; + state.credentials.set("9:owner", { + id: 9, + username: "host-user", + authType: "password", + password: "host-pass", + privateKey: null, + key: null, + keyPassword: null, + keyType: null, + }); + + const host = (await resolveHostById(42, "owner")) as Record< + string, + unknown + >; + expect(host.username).toBe("host-user"); + expect(host.password).toBe("host-pass"); + }); + + it("does not expose the owner's secret-backed SSH authentication", async () => { + expect(await resolveHostById(42, "recipient")).toBeNull(); + }); + + it("uses the owner-provided SSH snapshot when sharing is enabled", async () => { + state.host = baseHost({ shareSshAuth: true, username: "host-user" }); state.sharedSecret = { username: "shared-user", authType: "password", @@ -150,14 +219,56 @@ describe("resolveHostById", () => { string, unknown >; + expect(host.username).toBe("host-user"); expect(host.password).toBe("shared-pass"); - expect(host.username).toBe("shared-user"); - expect(host.sudoPassword).toBeNull(); - expect(host.autostartPassword).toBeNull(); + expect(host.authType).toBe("password"); }); - it("prefers the recipient's override credential over the snapshot", async () => { - state.host = baseHost({ username: "" }); + it("denies shared secret-backed auth when the opted-in snapshot is missing", async () => { + state.host = baseHost({ shareSshAuth: true }); + expect(await resolveHostById(42, "recipient")).toBeNull(); + }); + + it("keeps SSH agent authentication private unless the owner opts in", async () => { + state.host = baseHost({ + authType: "agent", + password: null, + terminalConfig: JSON.stringify({ + agentSocketPath: "/run/user/1000/ssh-agent.sock", + }), + }); + + expect(await resolveHostById(42, "recipient")).toBeNull(); + }); + + it("allows SSH agent authentication after the owner explicitly opts in", async () => { + state.host = baseHost({ + authType: "agent", + password: null, + shareSshAuth: true, + terminalConfig: JSON.stringify({ + agentSocketPath: "/run/user/1000/ssh-agent.sock", + }), + }); + + const host = (await resolveHostById(42, "recipient")) as Record< + string, + unknown + >; + expect(host.authType).toBe("agent"); + expect(host.terminalConfig).toEqual({ + agentSocketPath: "/run/user/1000/ssh-agent.sock", + sudoPassword: null, + }); + }); + + it("uses the recipient's credential instead of the owner's authentication", async () => { + state.host = baseHost({ username: "", shareSshAuth: true }); + state.sharedSecret = { + username: "shared-user", + authType: "password", + password: "shared-pass", + }; state.overrideCredentialId = 5; state.credentials.set("5:recipient", { id: 5, @@ -169,11 +280,6 @@ describe("resolveHostById", () => { keyPassword: null, keyType: null, }); - state.sharedSecret = { - username: "shared-user", - authType: "password", - password: "shared-pass", - }; const host = (await resolveHostById(42, "recipient")) as Record< string, @@ -183,14 +289,122 @@ describe("resolveHostById", () => { expect(host.username).toBe("my-user"); }); - it("denies a non-owner when a secret-bearing host has no snapshot", async () => { + it("uses the recipient credential username even when the owner forces their own credential username", async () => { + state.host = baseHost({ + username: "owner-login", + overrideCredentialUsername: true, + }); + state.overrideCredentialId = 5; + state.credentials.set("5:recipient", { + id: 5, + username: "recipient-login", + authType: "key", + password: null, + privateKey: "RECIPIENT-KEY", + key: null, + keyPassword: null, + keyType: "ssh-ed25519", + }); + + const host = (await resolveHostById(42, "recipient")) as Record< + string, + unknown + >; + expect(host.username).toBe("recipient-login"); + expect(host.authType).toBe("key"); + expect(host.key).toBe("RECIPIENT-KEY"); + }); + + it("fully replaces Vault authentication with the recipient override", async () => { + state.host = baseHost({ + authType: "vault", + password: null, + vaultProfileId: 7, + }); + state.vaultProfile = { id: 7 }; + state.overrideCredentialId = 5; + state.credentials.set("5:recipient", { + id: 5, + username: "recipient-login", + authType: "key", + password: null, + privateKey: "RECIPIENT-KEY", + key: null, + keyPassword: null, + keyType: "ssh-ed25519", + certPublicKey: "ssh-ed25519-cert-v01@example certificate", + }); + + const host = (await resolveHostById(42, "recipient")) as Record< + string, + unknown + >; + expect(host.authType).toBe("key"); + expect(host.key).toBe("RECIPIENT-KEY"); + expect(host.certPublicKey).toBe("ssh-ed25519-cert-v01@example certificate"); + expect(host.vaultProfile).toBeUndefined(); + }); + + it("falls back to the host username when the override credential has none", async () => { + state.host = baseHost({ username: "shared-login" }); + state.overrideCredentialId = 5; + state.credentials.set("5:recipient", { + id: 5, + username: null, + authType: "password", + password: "my-pass", + privateKey: null, + key: null, + keyPassword: null, + keyType: null, + }); + + const host = (await resolveHostById(42, "recipient")) as Record< + string, + unknown + >; + expect(host.username).toBe("shared-login"); + }); + + it("denies a non-owner when a secret-bearing host has no personal credential", async () => { + expect(await resolveHostById(42, "recipient")).toBeNull(); + }); + + it("ignores a stored override when shared access is inactive", async () => { + state.hasAccess = false; + state.overrideCredentialId = 5; + state.credentials.set("5:recipient", { + id: 5, + username: "recipient", + authType: "password", + password: "my-pass", + }); + expect(await resolveHostById(42, "recipient")).toBeNull(); }); it("lets a non-owner through on secret-less auth types without a snapshot", async () => { - state.host = baseHost({ authType: "none", password: null }); - const host = await resolveHostById(42, "recipient"); - expect(host).not.toBeNull(); + state.host = baseHost({ + authType: "none", + password: "stale-owner-password", + key: "stale-owner-key", + credentialId: null, + terminalConfig: JSON.stringify({ + theme: "termix", + sudoPassword: "owner-sudo", + }), + }); + const host = (await resolveHostById(42, "recipient")) as Record< + string, + unknown + >; + expect(host.password).toBeNull(); + expect(host.key).toBeNull(); + expect(host.credentialId).toBeNull(); + expect(host.terminalConfig).toEqual({ + theme: "termix", + sudoPassword: null, + }); }); it("resolves an admin bypass like the owner, keeping owner-only secrets", async () => { @@ -241,4 +455,26 @@ describe("resolveHostById", () => { await resolveHostById(42, "owner"); expect(state.auditCalls).toHaveLength(0); }); + + it("parses an empty port_knock_sequence '[]' string into an empty array (no bogus knock)", async () => { + state.host = baseHost({ portKnockSequence: "[]" }); + const host = (await resolveHostById(42, "owner")) as Record< + string, + unknown + >; + expect(host.portKnockSequence).toEqual([]); + }); + + it("parses a real port_knock_sequence JSON string into an array", async () => { + state.host = baseHost({ + portKnockSequence: '[{"port":1234,"protocol":"tcp","delay":100}]', + }); + const host = (await resolveHostById(42, "owner")) as Record< + string, + unknown + >; + expect(host.portKnockSequence).toEqual([ + { port: 1234, protocol: "tcp", delay: 100 }, + ]); + }); }); diff --git a/src/backend/tests/hosts/metrics/alert-engine-superseded.test.ts b/src/backend/tests/hosts/metrics/alert-engine-superseded.test.ts new file mode 100644 index 0000000..876a9ba --- /dev/null +++ b/src/backend/tests/hosts/metrics/alert-engine-superseded.test.ts @@ -0,0 +1,86 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * Once the alert rules have been migrated into automations, both systems hold + * a copy of every rule. If the old engine kept evaluating, every alert would + * be delivered twice. + */ + +const listEnabledRulesForHost = vi.fn(); +const listEnabledRulesForHostUser = vi.fn(); +const createFiring = vi.fn(); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentAlertRepository: () => ({ + listEnabledRulesForHost, + listEnabledRulesForHostUser, + createFiring, + pruneFiringsOlderThan: vi.fn(), + listEnabledChannelsForRule: vi.fn(async () => []), + findRuleById: vi.fn(async () => null), + getHostDisplayName: vi.fn(async () => "host"), + }), +})); + +vi.mock("../../../utils/logger.js", () => ({ + statsLogger: { warn: vi.fn(), info: vi.fn(), error: vi.fn() }, +})); + +vi.mock("../../../utils/notification-sender.js", () => ({ + sendNotification: vi.fn(async () => undefined), +})); + +vi.mock("../../../utils/discord-sender.js", () => ({ + sendDiscord: vi.fn(async () => undefined), +})); + +const alertEngineModule = + await import("../../../hosts/metrics/alert-engine.js"); + +const cpuRule = { + id: 1, + userId: "user-1", + hostId: null, + name: "CPU", + enabled: true, + triggerType: "cpu_threshold", + thresholdValue: 50, + thresholdDurationSeconds: 0, + cooldownMinutes: 0, +}; + +beforeEach(() => { + vi.clearAllMocks(); + listEnabledRulesForHost.mockResolvedValue([cpuRule]); + listEnabledRulesForHostUser.mockResolvedValue([cpuRule]); +}); + +describe("AlertEngine before migration", () => { + it("still evaluates rules", async () => { + expect(alertEngineModule.isAlertEngineSuperseded()).toBe(false); + + await alertEngineModule.AlertEngine.getInstance().evaluateMetrics(7, { + cpu: { percent: 90 }, + }); + + expect(listEnabledRulesForHost).toHaveBeenCalled(); + }); +}); + +describe("AlertEngine after migration", () => { + it("stops evaluating every trigger type", async () => { + alertEngineModule.markAlertEngineSuperseded(); + expect(alertEngineModule.isAlertEngineSuperseded()).toBe(true); + + const engine = alertEngineModule.AlertEngine.getInstance(); + await engine.evaluateMetrics(7, { cpu: { percent: 99 } }); + await engine.evaluateStatus(7, false); + await engine.evaluateHealthCheck(7, "user-1", "web", false); + await engine.evaluateUserLogin(7, "user-1", "root", "10.0.0.1"); + + // Nothing is even loaded, so nothing can be delivered a second time. + expect(listEnabledRulesForHost).not.toHaveBeenCalled(); + expect(listEnabledRulesForHostUser).not.toHaveBeenCalled(); + expect(createFiring).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/hosts/metrics/helpers.test.ts b/src/backend/tests/hosts/metrics/helpers.test.ts index 1fd0e28..0981c72 100644 --- a/src/backend/tests/hosts/metrics/helpers.test.ts +++ b/src/backend/tests/hosts/metrics/helpers.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, vi } from "vitest"; import { supportsMetrics, isTcpPingEnabled, + parseStatusHostIds, tcpPingThroughJumpHost, } from "../../../hosts/metrics/helpers.js"; import { createConnectionLog } from "../../../hosts/connection-log.js"; @@ -53,6 +54,17 @@ describe("isTcpPingEnabled", () => { }); }); +describe("parseStatusHostIds", () => { + it("distinguishes an unrestricted request from an empty host set", () => { + expect(parseStatusHostIds(undefined)).toBeNull(); + expect(parseStatusHostIds("")).toEqual(new Set()); + }); + + it("keeps only valid positive host IDs", () => { + expect(parseStatusHostIds("7,2,7,-1,nope,1.5")).toEqual(new Set([7, 2])); + }); +}); + describe("createConnectionLog", () => { it("builds a log entry without id/timestamp", () => { const entry = createConnectionLog("info", "connection", "Connecting", { diff --git a/src/backend/tests/hosts/metrics/login-alert-route-order.test.ts b/src/backend/tests/hosts/metrics/login-alert-route-order.test.ts new file mode 100644 index 0000000..11f4c42 --- /dev/null +++ b/src/backend/tests/hosts/metrics/login-alert-route-order.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import fs from "fs"; +import path from "path"; + +// Regression guard for: the /internal/login-alert route was registered +// after the global JWT auth middleware, so every service-to-service login +// alert got rejected with 401 before the route's own IP+token check ever +// ran. Spinning up the full metrics-service Express app (DB, SSH clients, +// polling managers, etc.) just to hit this one route is out of scope, so +// this asserts the registration order directly against the source instead. +describe("metrics service /internal/login-alert route order", () => { + it("is registered before the global auth middleware", () => { + const source = fs.readFileSync( + path.resolve(__dirname, "../../../hosts/metrics/index.ts"), + "utf8", + ); + + const routeIndex = source.indexOf('app.post("/internal/login-alert"'); + const authMiddlewareIndex = source.indexOf( + "app.use(authManager.createAuthMiddleware())", + ); + + expect(routeIndex).toBeGreaterThan(-1); + expect(authMiddlewareIndex).toBeGreaterThan(-1); + expect(routeIndex).toBeLessThan(authMiddlewareIndex); + }); +}); diff --git a/src/backend/tests/hosts/metrics/proxmox-stats-polling.test.ts b/src/backend/tests/hosts/metrics/proxmox-stats-polling.test.ts new file mode 100644 index 0000000..3dc9a3d --- /dev/null +++ b/src/backend/tests/hosts/metrics/proxmox-stats-polling.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +const historyCreate = vi.fn(); +const historyPrune = vi.fn(); +vi.mock("../../../database/repositories/factory.js", () => ({ + getCurrentSettingValue: () => null, + createCurrentProxmoxNodeHistoryRepository: () => ({ + create: historyCreate, + pruneOlderThan: historyPrune, + }), +})); + +const collectProxmoxStats = vi.fn(); +vi.mock("../../../hosts/metrics/proxmox/collect-proxmox-stats.js", () => ({ + collectProxmoxStats: (...args: unknown[]) => collectProxmoxStats(...args), +})); + +import { + ProxmoxPollingManager, + parseProxmoxStatsConfig, +} from "../../../hosts/metrics/proxmox-stats-polling.js"; +import type { Client } from "ssh2"; + +interface TestHost { + id: number; + userId: string; + proxmoxStatsConfig?: string | null; +} + +function snapshot(overrides: Partial> = {}) { + return { + node: { + cpu: { percent: 10, cores: 4, load: [0.1, 0.2, 0.3] }, + memory: { percent: 20, usedGiB: 2, totalGiB: 8 }, + disk: { percent: 30, usedGiB: 30, totalGiB: 100 }, + uptime: { seconds: 100, formatted: "0d 0h 1m" }, + system: { hostname: "pve1", kernel: "6.8", pveVersion: "8.2" }, + }, + network: { interfaces: [{ name: "eth0", rxBytes: "10", txBytes: "20" }] }, + guests: { guests: [], counts: { running: 0, stopped: 0, total: 0 } }, + storage: { pools: [] }, + cluster: { clustered: false }, + lastChecked: new Date().toISOString(), + ...overrides, + }; +} + +beforeEach(() => { + vi.useFakeTimers(); + historyCreate.mockReset(); + historyPrune.mockReset(); + collectProxmoxStats.mockReset(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("parseProxmoxStatsConfig", () => { + it("returns default poll interval when config is missing", () => { + expect(parseProxmoxStatsConfig(null)).toEqual({ + nodeName: null, + pollInterval: 60, + }); + }); + + it("parses a JSON string config", () => { + expect( + parseProxmoxStatsConfig('{"nodeName":"pve1","pollInterval":30}'), + ).toEqual({ nodeName: "pve1", pollInterval: 30 }); + }); + + it("falls back to defaults on malformed JSON", () => { + expect(parseProxmoxStatsConfig("{not json")).toEqual({ + nodeName: null, + pollInterval: 60, + }); + }); +}); + +describe("ProxmoxPollingManager", () => { + function makeManager(host: TestHost) { + const fetchHostById = vi.fn(async () => host); + const withSshConnection = vi.fn( + async (_host: TestHost, fn: (client: Client) => Promise) => + fn({} as Client), + ); + const manager = new ProxmoxPollingManager({ + fetchHostById, + withSshConnection, + }); + return { manager, fetchHostById, withSshConnection }; + } + + it("starts polling and caches a snapshot when the first viewer registers", async () => { + const host: TestHost = { id: 1, userId: "user-1" }; + collectProxmoxStats.mockResolvedValue(snapshot()); + const { manager, withSshConnection } = makeManager(host); + + manager.registerViewer(1, "viewer-1", "user-1"); + // registerViewer kicks off polling via a fire-and-forget promise chain. + await vi.waitFor(() => { + expect(withSshConnection).toHaveBeenCalled(); + }); + + const cached = manager.getStats(1); + expect(cached?.data.node.cpu.percent).toBe(10); + manager.destroy(); + }); + + it("stops polling once the last viewer unregisters", async () => { + const host: TestHost = { id: 2, userId: "user-1" }; + collectProxmoxStats.mockResolvedValue(snapshot()); + const { manager } = makeManager(host); + + manager.registerViewer(2, "viewer-a", "user-1"); + manager.registerViewer(2, "viewer-b", "user-1"); + await vi.waitFor(() => expect(manager.getStats(2)).toBeDefined()); + + manager.unregisterViewer(2, "viewer-a"); + // one viewer left - stats stay cached + expect(manager.getStats(2)).toBeDefined(); + + manager.unregisterViewer(2, "viewer-b"); + // Cached snapshot is retained (not cleared) but the interval is stopped; + // registering a new viewer must restart polling. + manager.destroy(); + }); + + it("updateHeartbeat returns false for an unknown session", () => { + const { manager } = makeManager({ id: 3, userId: "user-1" }); + expect(manager.updateHeartbeat("nope")).toBe(false); + manager.destroy(); + }); + + it("records an error snapshot when collection fails, without throwing", async () => { + const host: TestHost = { id: 4, userId: "user-1" }; + collectProxmoxStats.mockRejectedValue(new Error("pvesh not found")); + const { manager } = makeManager(host); + + manager.registerViewer(4, "viewer-1", "user-1"); + await vi.waitFor(() => { + expect(manager.getError(4)?.error).toBe("pvesh not found"); + }); + expect(manager.getStats(4)).toBeUndefined(); + manager.destroy(); + }); +}); diff --git a/src/backend/tests/hosts/metrics/proxmox/cluster-health-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/cluster-health-collector.test.ts new file mode 100644 index 0000000..c469ec2 --- /dev/null +++ b/src/backend/tests/hosts/metrics/proxmox/cluster-health-collector.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const execCommand = vi.fn(); +vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: (...args: unknown[]) => execCommand(...args), +})); + +import { collectProxmoxClusterHealth } from "../../../../hosts/metrics/proxmox/cluster-health-collector.js"; +import type { Client } from "ssh2"; + +const fakeClient = {} as Client; + +function result(stdout: string, code: number | null = 0) { + return { stdout, stderr: "", code }; +} + +beforeEach(() => { + execCommand.mockReset(); +}); + +describe("collectProxmoxClusterHealth", () => { + it("reports clustered:false for a standalone node (no cluster entry)", async () => { + execCommand.mockResolvedValueOnce(result(JSON.stringify([]), 0)); + const res = await collectProxmoxClusterHealth(fakeClient); + expect(res).toEqual({ clustered: false }); + }); + + it("parses a clustered response with quorum and node entries", async () => { + execCommand.mockResolvedValueOnce( + result( + JSON.stringify([ + { type: "cluster", name: "prod-cluster", quorate: 1, nodes: 3 }, + { type: "node", name: "pve1", online: 1, local: 1, ip: "10.0.0.1" }, + { type: "node", name: "pve2", online: 0, local: 0, ip: "10.0.0.2" }, + ]), + ), + ); + + const res = await collectProxmoxClusterHealth(fakeClient); + expect(res.clustered).toBe(true); + if (res.clustered) { + expect(res.quorate).toBe(true); + expect(res.clusterName).toBe("prod-cluster"); + expect(res.nodes).toHaveLength(2); + expect(res.nodes[0]).toEqual({ + name: "pve1", + online: true, + local: true, + ip: "10.0.0.1", + }); + expect(res.nodes[1].online).toBe(false); + } + }); + + it("returns clustered:false when pvesh fails", async () => { + execCommand.mockResolvedValueOnce(result("", 1)); + const res = await collectProxmoxClusterHealth(fakeClient); + expect(res).toEqual({ clustered: false }); + }); + + it("returns clustered:false on malformed JSON", async () => { + execCommand.mockResolvedValueOnce(result("not json", 0)); + const res = await collectProxmoxClusterHealth(fakeClient); + expect(res).toEqual({ clustered: false }); + }); +}); diff --git a/src/backend/tests/hosts/metrics/proxmox/collect-proxmox-stats.test.ts b/src/backend/tests/hosts/metrics/proxmox/collect-proxmox-stats.test.ts new file mode 100644 index 0000000..54a20ad --- /dev/null +++ b/src/backend/tests/hosts/metrics/proxmox/collect-proxmox-stats.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const execCommand = vi.fn(); +vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: (...args: unknown[]) => execCommand(...args), + toFixedNum: (n: number | null | undefined, digits = 2) => { + if (typeof n !== "number" || !Number.isFinite(n)) return null; + return Number(n.toFixed(digits)); + }, +})); + +import { collectProxmoxStats } from "../../../../hosts/metrics/proxmox/collect-proxmox-stats.js"; +import type { Client } from "ssh2"; + +const fakeClient = {} as Client; + +function result(stdout: string, code: number | null = 0) { + return { stdout, stderr: "", code }; +} + +beforeEach(() => { + execCommand.mockReset(); +}); + +describe("collectProxmoxStats", () => { + it("throws a distinguishable error when pvesh is missing", async () => { + execCommand.mockResolvedValueOnce(result("missing")); + + await expect(collectProxmoxStats(fakeClient, null)).rejects.toThrow( + /pvesh not found/i, + ); + // Only the pvesh-presence check should have run - no node resolution or + // per-collector execs once that check fails. + expect(execCommand).toHaveBeenCalledTimes(1); + }); + + it("auto-detects the node name via hostname when none is configured", async () => { + execCommand.mockResolvedValueOnce(result("ok")); // pvesh check + execCommand.mockResolvedValueOnce(result("pve-auto\n")); // hostname + // Five collectors run concurrently after that; give each a benign failing + // response so the aggregator still resolves with null-filled sub-shapes. + execCommand.mockResolvedValue(result("", 1)); + + const snapshot = await collectProxmoxStats(fakeClient, null); + expect(snapshot.lastChecked).toBeTruthy(); + expect(snapshot.node).toBeDefined(); + expect(snapshot.guests).toBeDefined(); + expect(snapshot.storage).toBeDefined(); + expect(snapshot.cluster).toEqual({ clustered: false }); + }); + + it("uses the configured node name when it is safe, skipping hostname detection", async () => { + execCommand.mockResolvedValueOnce(result("ok")); // pvesh check + execCommand.mockResolvedValue(result("", 1)); // all collector calls fail benignly + + await collectProxmoxStats(fakeClient, "my-node"); + + // hostname auto-detection command should never have been issued. + const calls = execCommand.mock.calls.map((c) => c[1] as string); + expect(calls).not.toContain("hostname"); + }); + + it("rejects an unsafe configured node name", async () => { + execCommand.mockResolvedValueOnce(result("ok")); // pvesh check + + await expect(collectProxmoxStats(fakeClient, "bad;node")).rejects.toThrow( + /valid Proxmox node name/i, + ); + }); +}); diff --git a/src/backend/tests/hosts/metrics/proxmox/guests-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/guests-collector.test.ts new file mode 100644 index 0000000..fbacffa --- /dev/null +++ b/src/backend/tests/hosts/metrics/proxmox/guests-collector.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const execCommand = vi.fn(); +vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: (...args: unknown[]) => execCommand(...args), + toFixedNum: (n: number | null | undefined, digits = 2) => { + if (typeof n !== "number" || !Number.isFinite(n)) return null; + return Number(n.toFixed(digits)); + }, +})); + +import { collectProxmoxGuestsSummary } from "../../../../hosts/metrics/proxmox/guests-collector.js"; +import type { Client } from "ssh2"; + +const fakeClient = {} as Client; + +function result(stdout: string, code: number | null = 0) { + return { stdout, stderr: "", code }; +} + +beforeEach(() => { + execCommand.mockReset(); +}); + +describe("collectProxmoxGuestsSummary", () => { + it("filters to the target node, excludes templates, and computes percentages", async () => { + execCommand.mockResolvedValueOnce( + result( + JSON.stringify([ + { + type: "qemu", + node: "pve1", + vmid: 100, + name: "vm-a", + status: "running", + cpu: 0.25, + mem: 2 * 1024 ** 3, + maxmem: 4 * 1024 ** 3, + disk: 10 * 1024 ** 3, + maxdisk: 40 * 1024 ** 3, + uptime: 3600, + }, + { + type: "lxc", + node: "pve1", + vmid: 101, + name: "ct-b", + status: "stopped", + cpu: 0, + mem: 0, + maxmem: 512 * 1024 ** 2, + disk: 0, + maxdisk: 8 * 1024 ** 3, + uptime: 0, + }, + { + // different node - excluded + type: "qemu", + node: "pve2", + vmid: 200, + name: "elsewhere", + status: "running", + }, + { + // template - excluded + type: "qemu", + node: "pve1", + vmid: 999, + name: "template", + status: "stopped", + template: 1, + }, + { + // non-guest resource type - excluded + type: "storage", + node: "pve1", + }, + ]), + ), + ); + + const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); + + expect(res.guests).toHaveLength(2); + expect(res.counts).toEqual({ running: 1, stopped: 1, total: 2 }); + + const vmA = res.guests.find((g) => g.vmid === 100)!; + expect(vmA.cpuPercent).toBe(25); + expect(vmA.memPercent).toBe(50); + expect(vmA.diskPercent).toBe(25); + }); + + it("reports null disk fields (not a false 0%) when maxdisk is 0", async () => { + execCommand.mockResolvedValueOnce( + result( + JSON.stringify([ + { + type: "qemu", + node: "pve1", + vmid: 100, + name: "vm-no-agent", + status: "running", + cpu: 0.1, + mem: 1024, + maxmem: 2048, + disk: 0, + maxdisk: 0, + uptime: 10, + }, + ]), + ), + ); + + const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); + expect(res.guests[0].diskPercent).toBeNull(); + expect(res.guests[0].diskUsedGiB).toBeNull(); + expect(res.guests[0].diskTotalGiB).toBeNull(); + }); + + it("returns an empty guest list when there are no matching resources", async () => { + execCommand.mockResolvedValueOnce(result(JSON.stringify([]), 0)); + const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); + expect(res.guests).toEqual([]); + expect(res.counts).toEqual({ running: 0, stopped: 0, total: 0 }); + }); + + it("returns an empty result when pvesh is missing (non-zero exit)", async () => { + execCommand.mockResolvedValueOnce(result("", 127)); + const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); + expect(res.guests).toEqual([]); + }); + + it("returns an empty result on malformed JSON", async () => { + execCommand.mockResolvedValueOnce(result("{not json", 0)); + const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); + expect(res.guests).toEqual([]); + }); + + it("rejects an unsafe node name before running any command", async () => { + const res = await collectProxmoxGuestsSummary(fakeClient, "../etc"); + expect(res.guests).toEqual([]); + expect(execCommand).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/hosts/metrics/proxmox/node-network-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/node-network-collector.test.ts new file mode 100644 index 0000000..b120cc7 --- /dev/null +++ b/src/backend/tests/hosts/metrics/proxmox/node-network-collector.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const execCommand = vi.fn(); +vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: (...args: unknown[]) => execCommand(...args), +})); + +import { collectProxmoxNodeNetwork } from "../../../../hosts/metrics/proxmox/node-network-collector.js"; +import type { Client } from "ssh2"; + +const fakeClient = {} as Client; + +function result(stdout: string, code: number | null = 0) { + return { stdout, stderr: "", code }; +} + +beforeEach(() => { + execCommand.mockReset(); +}); + +describe("collectProxmoxNodeNetwork", () => { + it("rejects an unsafe node name before running any command", async () => { + const res = await collectProxmoxNodeNetwork(fakeClient, "bad;name"); + expect(res.interfaces).toEqual([]); + expect(execCommand).not.toHaveBeenCalled(); + }); + + it("falls back to /proc/net/dev when pvesh netstat fails", async () => { + // First call: pvesh netstat -> failure. + execCommand.mockResolvedValueOnce(result("", 1)); + // Fallback calls: ip addr, ip link, /proc/net/dev. + execCommand.mockResolvedValueOnce(result("eth0 10.0.0.5/24\n")); + execCommand.mockResolvedValueOnce(result("eth0 UP\n")); + execCommand.mockResolvedValueOnce( + result( + "Inter-| Receive\n" + + " face |bytes packets\n" + + "eth0: 123456 10 0 0 0 0 0 0 654321 20 0 0 0 0 0 0\n", + ), + ); + + const res = await collectProxmoxNodeNetwork(fakeClient, "pve1"); + expect(res.interfaces).toHaveLength(1); + expect(res.interfaces[0]).toMatchObject({ + name: "eth0", + ip: "10.0.0.5", + state: "UP", + rxBytes: "123456", + txBytes: "654321", + }); + }); + + it("falls back to /proc/net/dev when pvesh returns unparseable data", async () => { + execCommand.mockResolvedValueOnce(result("not json", 0)); + execCommand.mockResolvedValueOnce(result("")); + execCommand.mockResolvedValueOnce(result("")); + execCommand.mockResolvedValueOnce(result("Inter-| Receive\n face |\n")); + + const res = await collectProxmoxNodeNetwork(fakeClient, "pve1"); + expect(res.interfaces).toEqual([]); + }); +}); diff --git a/src/backend/tests/hosts/metrics/proxmox/node-status-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/node-status-collector.test.ts new file mode 100644 index 0000000..79113fc --- /dev/null +++ b/src/backend/tests/hosts/metrics/proxmox/node-status-collector.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const execCommand = vi.fn(); +vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: (...args: unknown[]) => execCommand(...args), + toFixedNum: (n: number | null | undefined, digits = 2) => { + if (typeof n !== "number" || !Number.isFinite(n)) return null; + return Number(n.toFixed(digits)); + }, +})); + +import { collectProxmoxNodeStatus } from "../../../../hosts/metrics/proxmox/node-status-collector.js"; +import type { Client } from "ssh2"; + +const fakeClient = {} as Client; + +function result(stdout: string, code: number | null = 0) { + return { stdout, stderr: "", code }; +} + +beforeEach(() => { + execCommand.mockReset(); +}); + +describe("collectProxmoxNodeStatus", () => { + it("parses a healthy pvesh /nodes/{node}/status response", async () => { + execCommand.mockResolvedValueOnce( + result( + JSON.stringify({ + cpu: 0.15, + cpuinfo: { cores: 8 }, + loadavg: ["0.5", "0.6", "0.7"], + memory: { used: 4 * 1024 ** 3, total: 16 * 1024 ** 3 }, + rootfs: { used: 20 * 1024 ** 3, total: 100 * 1024 ** 3 }, + uptime: 90061, + hostname: "pve1", + kversion: "Linux 6.8.0", + pveversion: "pve-manager/8.2.0", + }), + ), + ); + + const res = await collectProxmoxNodeStatus(fakeClient, "pve1"); + + expect(res.cpu.percent).toBe(15); + expect(res.cpu.cores).toBe(8); + expect(res.cpu.load).toEqual([0.5, 0.6, 0.7]); + expect(res.memory.percent).toBe(25); + expect(res.memory.usedGiB).toBeCloseTo(4, 1); + expect(res.memory.totalGiB).toBeCloseTo(16, 1); + expect(res.disk.percent).toBe(20); + expect(res.uptime.seconds).toBe(90061); + expect(res.uptime.formatted).toBe("1d 1h 1m"); + expect(res.system.hostname).toBe("pve1"); + expect(res.system.kernel).toBe("Linux 6.8.0"); + expect(res.system.pveVersion).toBe("pve-manager/8.2.0"); + }); + + it("returns a fully null-filled shape when pvesh exits non-zero", async () => { + execCommand.mockResolvedValueOnce(result("", 1)); + const res = await collectProxmoxNodeStatus(fakeClient, "pve1"); + expect(res.cpu.percent).toBeNull(); + expect(res.memory.percent).toBeNull(); + expect(res.disk.percent).toBeNull(); + expect(res.uptime.seconds).toBeNull(); + expect(res.system.hostname).toBeNull(); + }); + + it("returns null-filled shape on malformed JSON", async () => { + execCommand.mockResolvedValueOnce(result("not json", 0)); + const res = await collectProxmoxNodeStatus(fakeClient, "pve1"); + expect(res.cpu.percent).toBeNull(); + }); + + it("rejects an unsafe node name before running any command", async () => { + const res = await collectProxmoxNodeStatus(fakeClient, "pve1; rm -rf /"); + expect(res.cpu.percent).toBeNull(); + expect(execCommand).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/hosts/metrics/proxmox/storage-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/storage-collector.test.ts new file mode 100644 index 0000000..857d551 --- /dev/null +++ b/src/backend/tests/hosts/metrics/proxmox/storage-collector.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const execCommand = vi.fn(); +vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ + execCommand: (...args: unknown[]) => execCommand(...args), + toFixedNum: (n: number | null | undefined, digits = 2) => { + if (typeof n !== "number" || !Number.isFinite(n)) return null; + return Number(n.toFixed(digits)); + }, +})); + +import { collectProxmoxStorage } from "../../../../hosts/metrics/proxmox/storage-collector.js"; +import type { Client } from "ssh2"; + +const fakeClient = {} as Client; + +function result(stdout: string, code: number | null = 0) { + return { stdout, stderr: "", code }; +} + +beforeEach(() => { + execCommand.mockReset(); +}); + +describe("collectProxmoxStorage", () => { + it("parses storage pools with usage percentages", async () => { + execCommand.mockResolvedValueOnce( + result( + JSON.stringify([ + { + storage: "local", + type: "dir", + active: 1, + enabled: 1, + used: 20 * 1024 ** 3, + total: 100 * 1024 ** 3, + avail: 80 * 1024 ** 3, + }, + { + storage: "local-zfs", + type: "zfspool", + active: 0, + enabled: 1, + used: 0, + total: 0, + avail: 0, + }, + ]), + ), + ); + + const res = await collectProxmoxStorage(fakeClient, "pve1"); + expect(res.pools).toHaveLength(2); + expect(res.pools[0].name).toBe("local"); + expect(res.pools[0].active).toBe(true); + expect(res.pools[0].percent).toBe(20); + expect(res.pools[1].active).toBe(false); + expect(res.pools[1].percent).toBeNull(); + }); + + it("returns an empty pool list when pvesh fails", async () => { + execCommand.mockResolvedValueOnce(result("", 1)); + const res = await collectProxmoxStorage(fakeClient, "pve1"); + expect(res.pools).toEqual([]); + }); + + it("returns an empty pool list on malformed JSON", async () => { + execCommand.mockResolvedValueOnce(result("nope", 0)); + const res = await collectProxmoxStorage(fakeClient, "pve1"); + expect(res.pools).toEqual([]); + }); + + it("rejects an unsafe node name before running any command", async () => { + const res = await collectProxmoxStorage(fakeClient, "$(whoami)"); + expect(res.pools).toEqual([]); + expect(execCommand).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/hosts/metrics/state.test.ts b/src/backend/tests/hosts/metrics/state.test.ts index 6406808..e49ba6d 100644 --- a/src/backend/tests/hosts/metrics/state.test.ts +++ b/src/backend/tests/hosts/metrics/state.test.ts @@ -1,9 +1,22 @@ import { describe, expect, it, vi } from "vitest"; import { + canStartInitialMetrics, ConcurrentLimiter, HostPollCache, + metricsConcurrencyFor, } from "../../../hosts/metrics/state.js"; +describe("initial metrics admission", () => { + it("requires both an active viewer and a confirmed online status", () => { + expect(canStartInitialMetrics("online", true)).toBe(true); + expect(canStartInitialMetrics("reachable", true)).toBe(true); + expect(canStartInitialMetrics("offline", true)).toBe(false); + expect(canStartInitialMetrics(undefined, true)).toBe(false); + expect(canStartInitialMetrics("online", false)).toBe(false); + expect(canStartInitialMetrics(undefined, true, false)).toBe(true); + }); +}); + describe("ConcurrentLimiter", () => { it("never exceeds max concurrent runners", async () => { const limiter = new ConcurrentLimiter(2); @@ -51,6 +64,161 @@ describe("ConcurrentLimiter", () => { it("rejects invalid maxConcurrent", () => { expect(() => new ConcurrentLimiter(0)).toThrow(/maxConcurrent/); }); + + describe("setLimit", () => { + it("releases queued waiters as soon as the ceiling is raised", async () => { + const limiter = new ConcurrentLimiter(1); + let running = 0; + let peak = 0; + const release: Array<() => void> = []; + + const job = () => + limiter.run(async () => { + running += 1; + peak = Math.max(peak, running); + await new Promise((r) => release.push(r)); + running -= 1; + }); + + const jobs = [job(), job(), job(), job()]; + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(1); + expect(limiter.pendingCount).toBe(3); + + // Widening must drain the backlog without waiting for the running job. + limiter.setLimit(4); + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(4); + expect(limiter.pendingCount).toBe(0); + + release.forEach((fn) => fn()); + await Promise.all(jobs); + expect(limiter.activeCount).toBe(0); + }); + + it("does not over-release beyond the new ceiling", async () => { + const limiter = new ConcurrentLimiter(1); + let running = 0; + let peak = 0; + const release: Array<() => void> = []; + // Later waves of woken jobs enqueue their own resolvers, so draining has + // to keep going until nothing is left rather than flushing a snapshot. + const drain = async () => { + while (release.length > 0) { + release.splice(0).forEach((fn) => fn()); + await new Promise((r) => setTimeout(r, 5)); + } + }; + + const job = () => + limiter.run(async () => { + running += 1; + peak = Math.max(peak, running); + await new Promise((r) => release.push(r)); + running -= 1; + }); + + const jobs = [job(), job(), job(), job(), job()]; + await new Promise((r) => setTimeout(r, 10)); + + limiter.setLimit(3); + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(3); + expect(limiter.pendingCount).toBe(2); + + await drain(); + await Promise.all(jobs); + expect(limiter.activeCount).toBe(0); + expect(limiter.pendingCount).toBe(0); + }); + + it("lets running work finish when the ceiling shrinks", async () => { + const limiter = new ConcurrentLimiter(4); + let running = 0; + let peak = 0; + const release: Array<() => void> = []; + const drain = async () => { + while (release.length > 0) { + release.splice(0).forEach((fn) => fn()); + await new Promise((r) => setTimeout(r, 5)); + } + }; + + const job = () => + limiter.run(async () => { + running += 1; + peak = Math.max(peak, running); + await new Promise((r) => release.push(r)); + running -= 1; + }); + + const jobs = [job(), job(), job(), job(), job(), job()]; + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(4); + + // Shrinking never kills in-flight work; it applies to later releases. + limiter.setLimit(2); + expect(limiter.activeCount).toBe(4); + + await drain(); + await Promise.all(jobs); + expect(limiter.activeCount).toBe(0); + expect(limiter.pendingCount).toBe(0); + // The two queued jobs ran only after the shrink, so they never pushed + // occupancy back up to the old width. + expect(peak).toBe(4); + }); + + it("rejects an invalid new limit", () => { + const limiter = new ConcurrentLimiter(2); + expect(() => limiter.setLimit(0)).toThrow(/maxConcurrent/); + expect(limiter.limit).toBe(2); + }); + }); +}); + +describe("metricsConcurrencyFor", () => { + it("keeps a floor for small installs", () => { + expect(metricsConcurrencyFor(0, {})).toBe(5); + expect(metricsConcurrencyFor(1, {})).toBe(5); + expect(metricsConcurrencyFor(60, {})).toBe(5); + }); + + it("scales up with the fleet", () => { + expect(metricsConcurrencyFor(200, {})).toBe(10); + expect(metricsConcurrencyFor(500, {})).toBe(25); + }); + + it("caps so a huge fleet cannot exhaust the host", () => { + expect(metricsConcurrencyFor(100000, {})).toBe(50); + }); + + it("lets an operator override the sizing", () => { + expect(metricsConcurrencyFor(1000, { METRICS_POLL_CONCURRENCY: "8" })).toBe( + 8, + ); + }); + + it("still caps an oversized override", () => { + expect( + metricsConcurrencyFor(10, { METRICS_POLL_CONCURRENCY: "9999" }), + ).toBe(50); + }); + + it("ignores a nonsense override", () => { + expect( + metricsConcurrencyFor(500, { METRICS_POLL_CONCURRENCY: "abc" }), + ).toBe(25); + }); + + it("sweeps 500 hosts inside a 30s interval, which the old fixed 5 could not", () => { + const POLL_MS = 400; + const hosts = 500; + const sweepAt = (c: number) => Math.ceil(hosts / c) * POLL_MS; + + expect(sweepAt(5)).toBeGreaterThan(30_000); + expect(sweepAt(metricsConcurrencyFor(hosts, {}))).toBeLessThan(30_000); + }); }); describe("HostPollCache", () => { diff --git a/src/backend/tests/hosts/metrics/widgets/disk-collector.test.ts b/src/backend/tests/hosts/metrics/widgets/disk-collector.test.ts new file mode 100644 index 0000000..4ae1d5e --- /dev/null +++ b/src/backend/tests/hosts/metrics/widgets/disk-collector.test.ts @@ -0,0 +1,224 @@ +import { describe, it, expect } from "vitest"; +import { + parseDfLines, + findWorstMountIndex, + buildFilesystemList, + selectPrimaryFilesystem, + filterExcludedFilesystems, + mergeMonitoredFilesystems, +} from "../../../../hosts/metrics/widgets/disk-collector.js"; + +describe("parseDfLines", () => { + it("parses df -T -P output into rows", () => { + const output = + "/dev/nvme0n1p2 ext4 3848290697216 1046898851840 2606516101120 29% /\n" + + "/dev/nvme1n1p1 ext4 15393162788864 15239230844928 153931922841 99% /data\n"; + const rows = parseDfLines(output); + expect(rows).toHaveLength(2); + expect(rows[0].mount).toBe("/"); + expect(rows[0].type).toBe("ext4"); + expect(rows[1].mount).toBe("/data"); + }); + + it("filters out pseudo filesystems", () => { + const output = + "tmpfs tmpfs 8000 0 8000 0% /dev/shm\n" + + "overlay overlay 100 50 50 50% /\n" + + "/dev/sda1 ext4 100 50 50 50% /mnt/data\n"; + const rows = parseDfLines(output); + expect(rows).toHaveLength(1); + expect(rows[0].mount).toBe("/mnt/data"); + }); + + it("captures the filesystem type for network shares", () => { + const output = + "nas.local:/export nfs4 2000 1900 100 95% /mnt/nas\n" + + "//server/share cifs 2000 1000 1000 50% /mnt/smb\n"; + const rows = parseDfLines(output); + expect(rows[0].type).toBe("nfs4"); + expect(rows[1].type).toBe("cifs"); + }); +}); + +describe("findWorstMountIndex", () => { + it("picks the most-utilized mount, not just the first row", () => { + const rows = parseDfLines( + "/dev/nvme0n1p2 ext4 3848290697216 1046898851840 2606516101120 29% /\n" + + "/dev/nvme1n1p1 ext4 15393162788864 15239230844928 153931922841 99% /data\n", + ); + const worst = findWorstMountIndex(rows); + expect(worst.index).toBe(1); + expect(worst.totalBytes).toBe(15393162788864); + expect(worst.usedBytes).toBe(15239230844928); + }); + + it("falls back to the only mount available", () => { + const rows = parseDfLines("/dev/sda1 ext4 100 30 70 30% /\n"); + const worst = findWorstMountIndex(rows); + expect(worst.index).toBe(0); + }); + + it("skips rows with invalid or zero totals", () => { + const rows = parseDfLines( + "/dev/sda1 ext4 0 0 0 0% /broken\n" + + "/dev/sda2 ext4 100 40 60 40% /ok\n", + ); + const worst = findWorstMountIndex(rows); + expect(worst.index).toBe(1); + }); + + it("returns index -1 when there are no usable rows", () => { + const worst = findWorstMountIndex([]); + expect(worst.index).toBe(-1); + expect(worst.totalBytes).toBe(0); + }); +}); + +const BYTES_OUTPUT = + "/dev/nvme0n1p2 ext4 1000 400 600 40% /\n" + + "/dev/nvme1n1p1 ext4 2000 1900 100 95% /data\n"; +const HUMAN_OUTPUT = + "/dev/nvme0n1p2 ext4 1.0K 400 600 40% /\n" + + "/dev/nvme1n1p1 ext4 2.0K 1.9K 100 95% /data\n"; + +describe("buildFilesystemList", () => { + it("returns every real filesystem with byte maths and human strings", () => { + const list = buildFilesystemList( + parseDfLines(BYTES_OUTPUT), + parseDfLines(HUMAN_OUTPUT), + ); + expect(list).toHaveLength(2); + expect(list[0]).toMatchObject({ + mount: "/", + type: "ext4", + percent: 40, + usedHuman: "400", + totalHuman: "1.0K", + availableHuman: "600", + usedBytes: 400, + totalBytes: 1000, + }); + expect(list[1]).toMatchObject({ mount: "/data", percent: 95 }); + }); + + it("matches human rows by mount when the row counts differ", () => { + const list = buildFilesystemList( + parseDfLines(BYTES_OUTPUT), + parseDfLines("/dev/nvme1n1p1 ext4 2.0K 1.9K 100 95% /data\n"), + ); + expect(list[0].totalHuman).toBeNull(); + expect(list[1].totalHuman).toBe("2.0K"); + }); + + it("drops filesystems with a zero or invalid total", () => { + const list = buildFilesystemList( + parseDfLines( + "/dev/sda1 ext4 0 0 0 0% /broken\n/dev/sda2 ext4 100 40 60 40% /ok\n", + ), + [], + ); + expect(list).toHaveLength(1); + expect(list[0].mount).toBe("/ok"); + }); +}); + +describe("selectPrimaryFilesystem", () => { + it("prefers root over a fuller secondary mount", () => { + const list = buildFilesystemList( + parseDfLines(BYTES_OUTPUT), + parseDfLines(HUMAN_OUTPUT), + ); + expect(selectPrimaryFilesystem(list)?.mount).toBe("/"); + }); + + it("falls back to the most-utilized mount when there is no root", () => { + const list = buildFilesystemList( + parseDfLines( + "/dev/sda1 ext4 1000 100 900 10% /mnt/a\n" + + "/dev/sda2 ext4 1000 800 200 80% /mnt/b\n", + ), + [], + ); + expect(selectPrimaryFilesystem(list)?.mount).toBe("/mnt/b"); + }); + + it("returns null for an empty list", () => { + expect(selectPrimaryFilesystem([])).toBeNull(); + }); +}); + +describe("filterExcludedFilesystems", () => { + const list = buildFilesystemList( + parseDfLines( + "/dev/sda1 ext4 1000 400 600 40% /\n" + + "nas.local:/export nfs4 2000 1900 100 95% /mnt/nas\n" + + "//server/share cifs 2000 1000 1000 50% /mnt/smb\n", + ), + [], + ); + + it("returns the same list when no mounts are excluded", () => { + expect(filterExcludedFilesystems(list)).toHaveLength(3); + expect(filterExcludedFilesystems(list, [])).toHaveLength(3); + }); + + it("excludes an exact mount path match", () => { + const filtered = filterExcludedFilesystems(list, ["/mnt/nas"]); + expect(filtered.map((fs) => fs.mount)).toEqual(["/", "/mnt/smb"]); + }); + + it("excludes by filesystem type substring, case-insensitively", () => { + const filtered = filterExcludedFilesystems(list, ["NFS"]); + expect(filtered.map((fs) => fs.mount)).toEqual(["/", "/mnt/smb"]); + }); + + it("supports excluding multiple network filesystem types at once", () => { + const filtered = filterExcludedFilesystems(list, ["nfs", "cifs"]); + expect(filtered.map((fs) => fs.mount)).toEqual(["/"]); + }); + + it("ignores blank/whitespace-only entries", () => { + const filtered = filterExcludedFilesystems(list, [" ", ""]); + expect(filtered).toHaveLength(3); + }); +}); + +describe("mergeMonitoredFilesystems", () => { + it("adds an arbitrary path with a user label", () => { + const detected = buildFilesystemList( + parseDfLines("/dev/sda1 ext4 1000 400 600 40% /\n"), + [], + ); + const custom = buildFilesystemList( + parseDfLines("/dev/sda1 ext4 1000 400 600 40% /\n"), + [], + ); + const result = mergeMonitoredFilesystems( + detected, + [{ path: "/config", label: "Home Assistant" }], + custom, + ); + + expect(result).toHaveLength(2); + expect(result[1]).toMatchObject({ + mount: "/config", + label: "Home Assistant", + totalBytes: 1000, + }); + }); + + it("labels a path that is already a detected mount", () => { + const detected = buildFilesystemList( + parseDfLines("/dev/sda1 ext4 1000 400 600 40% /data\n"), + [], + ); + const result = mergeMonitoredFilesystems( + detected, + [{ path: "/data", label: "Media" }], + detected, + ); + + expect(result).toHaveLength(1); + expect(result[0].label).toBe("Media"); + }); +}); diff --git a/src/backend/tests/hosts/metrics/widgets/network-collector.test.ts b/src/backend/tests/hosts/metrics/widgets/network-collector.test.ts new file mode 100644 index 0000000..d200f77 --- /dev/null +++ b/src/backend/tests/hosts/metrics/widgets/network-collector.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { + counterRate, + parseNetworkCounters, +} from "../../../../hosts/metrics/widgets/network-collector.js"; + +const PROC_NET = `Inter-| Receive | Transmit + face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed + eth0: 1024 1 0 0 0 0 0 0 2048 2 0 0 0 0 0 0 + lo: 4096 4 0 0 0 0 0 0 4096 4 0 0 0 0 0 0`; + +describe("network counters", () => { + it("parses Linux proc counters", () => { + expect(parseNetworkCounters(PROC_NET).get("eth0")).toEqual({ + rx: "1024", + tx: "2048", + }); + }); + + it("calculates bytes per second and rejects counter resets", () => { + expect(counterRate("1000", "2500", 0.5)).toBe(3000); + expect(counterRate("2500", "1000", 0.5)).toBeNull(); + }); +}); diff --git a/src/backend/tests/hosts/session-sharing/routes.test.ts b/src/backend/tests/hosts/session-sharing/routes.test.ts new file mode 100644 index 0000000..052d170 --- /dev/null +++ b/src/backend/tests/hosts/session-sharing/routes.test.ts @@ -0,0 +1,513 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Request, Response } from "express"; + +const state = vi.hoisted(() => ({ + currentUserId: "user-1", + globalSharingEnabled: true, + hosts: new Map(), + hostOwnerAccess: new Map(), // `${userId}:${hostId}` -> hasAccess + sshSessions: new Map(), + guacSessions: new Map< + string, + { ownerUserId: string; hostId: number; protocol: string } + >(), + shares: new Map>(), + admins: new Set(), +})); + +vi.mock("../../../utils/logger.js", () => ({ + sshLogger: { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + success: vi.fn(), + }, +})); + +vi.mock("../../../utils/auth-manager.js", () => ({ + AuthManager: { + getInstance: () => ({ + createAuthMiddleware: + () => + (req: Record, _res: unknown, next: () => void) => { + req.userId = state.currentUserId; + next(); + }, + }), + }, +})); + +vi.mock("../../../utils/permission-manager.js", () => ({ + PermissionManager: { + getInstance: () => ({ + canAccessHost: async ( + userId: string, + hostId: number, + _action: string, + ) => ({ + hasAccess: state.hostOwnerAccess.get(`${userId}:${hostId}`) ?? false, + }), + isAdmin: async (userId: string) => state.admins.has(userId), + }), + }, +})); + +vi.mock("../../../hosts/terminal/session-manager.js", () => ({ + sessionManager: { + getSession: (sessionId: string) => { + const session = state.sshSessions.get(sessionId); + if (!session) return null; + return { ...session }; + }, + ownerEndSession: vi.fn(), + }, +})); + +vi.mock("../../../hosts/guacamole/guacamole-server.js", () => ({ + getGuacSessionInfo: (guacamoleConnectionId: string) => + state.guacSessions.get(guacamoleConnectionId) ?? null, +})); + +vi.mock("../../../hosts/guacamole/token-service.js", () => ({ + GuacamoleTokenService: { + getInstance: () => ({ + createJoinToken: (guacamoleConnectionId: string, readOnly: boolean) => + `join-token:${guacamoleConnectionId}:${readOnly}`, + }), + }, +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentSessionShareRepository: () => ({ + create: async (input: Record) => { + const row = { + ...input, + createdAt: "2026-07-20T00:00:00.000Z", + revokedAt: null, + lastJoinedAt: null, + joinCount: 0, + }; + state.shares.set(input.id as string, row); + return row; + }, + findById: async (id: string) => state.shares.get(id) ?? null, + findByLinkToken: async (linkToken: string) => { + for (const share of state.shares.values()) { + if ( + share.linkToken === linkToken && + !share.revokedAt && + (share.expiresAt as string) > new Date().toISOString() + ) { + return share; + } + } + return null; + }, + findActiveSharesForHost: async (hostId: number, ownerUserId: string) => { + return [...state.shares.values()].filter( + (s) => + s.hostId === hostId && s.ownerUserId === ownerUserId && !s.revokedAt, + ); + }, + revoke: async (shareId: string, requestingUserId: string) => { + const share = state.shares.get(shareId); + if (!share || share.ownerUserId !== requestingUserId) return false; + share.revokedAt = "2026-07-20T01:00:00.000Z"; + return true; + }, + revokeAsAdmin: async (shareId: string) => { + const share = state.shares.get(shareId); + if (!share) return false; + share.revokedAt = "2026-07-20T01:00:00.000Z"; + return true; + }, + touchShareUsage: async () => {}, + recordParticipantJoin: async () => ({ id: 1 }), + }), + createCurrentSettingsRepository: () => ({ + getBoolean: async () => state.globalSharingEnabled, + }), + createCurrentHostResolutionRepository: () => ({ + findHostOwnerId: async (hostId: number) => + state.hosts.get(hostId)?.userId ?? null, + findHostById: async (hostId: number) => { + const host = state.hosts.get(hostId); + if (!host) return null; + return { allowSessionSharing: host.allowSessionSharing }; + }, + }), +})); + +const { default: router } = + await import("../../../hosts/session-sharing/routes.js"); + +type RouteLayer = { + route?: { + path: string; + methods: Record; + stack: { + handle: (req: Request, res: Response, next: () => void) => unknown; + }[]; + }; +}; + +function findHandlers(method: string, path: string) { + const layers = (router as unknown as { stack: RouteLayer[] }).stack; + const layer = layers.find( + (l) => l.route?.path === path && l.route.methods[method], + ); + if (!layer?.route) throw new Error(`No route for ${method} ${path}`); + return layer.route.stack.map((s) => s.handle); +} + +function makeReqRes(overrides: { + body?: Record; + params?: Record; + ip?: string; +}) { + const req = { + body: overrides.body ?? {}, + params: overrides.params ?? {}, + headers: {}, + ip: overrides.ip ?? "127.0.0.1", + socket: { remoteAddress: overrides.ip ?? "127.0.0.1" }, + } as unknown as Request; + + const res = { + statusCode: 200, + jsonBody: null as unknown, + status(code: number) { + (this as unknown as { statusCode: number }).statusCode = code; + return this; + }, + json(payload: unknown) { + (this as unknown as { jsonBody: unknown }).jsonBody = payload; + return this; + }, + } as unknown as Response & { statusCode: number; jsonBody: unknown }; + + return { req, res }; +} + +async function invoke( + method: string, + path: string, + overrides: { + body?: Record; + params?: Record; + ip?: string; + } = {}, +) { + const handlers = findHandlers(method, path); + const { req, res } = makeReqRes(overrides); + + for (const handler of handlers) { + let calledNext = false; + await handler(req, res, () => { + calledNext = true; + }); + if (!calledNext) break; + } + + return res as unknown as { + statusCode: number; + jsonBody: Record | null; + }; +} + +beforeEach(() => { + state.currentUserId = "user-1"; + state.globalSharingEnabled = true; + state.hosts = new Map([ + [1, { userId: "user-1", allowSessionSharing: true }], + [2, { userId: "user-1", allowSessionSharing: false }], + ]); + state.hostOwnerAccess = new Map([["user-2:1", true]]); + state.sshSessions = new Map([ + ["session-1", { userId: "user-1", isConnected: true }], + ]); + state.guacSessions = new Map([ + ["guac-conn-1", { ownerUserId: "user-1", hostId: 1, protocol: "vnc" }], + ]); + state.shares = new Map(); + state.admins = new Set(); +}); + +describe("POST /session-sharing/create", () => { + it("rejects a caller who does not own the live session", async () => { + state.currentUserId = "user-2"; + const res = await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + }, + }); + + expect(res.statusCode).toBe(403); + expect(res.jsonBody).toMatchObject({ + error: "You do not own this live session", + }); + }); + + it("creates a link share for the session owner", async () => { + const res = await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.jsonBody).toMatchObject({ shareId: expect.any(String) }); + expect((res.jsonBody as Record).linkToken).toBeTruthy(); + }); + + it("rejects a user share when the target lacks host access", async () => { + const res = await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "user", + targetUserId: "no-access-user", + permissionLevel: "read-write", + }, + }); + + expect(res.statusCode).toBe(403); + expect(res.jsonBody).toMatchObject({ + error: "Target user does not have access to this host", + }); + }); + + it("global kill switch overrides an enabled per-host toggle", async () => { + state.globalSharingEnabled = false; + const res = await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + }, + }); + + expect(res.statusCode).toBe(403); + expect(res.jsonBody).toMatchObject({ + error: "Session sharing is disabled for this host", + }); + }); + + it("rejects when the per-host toggle is off even though global is on", async () => { + const res = await invoke("post", "/create", { + body: { + hostId: 2, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + }, + }); + + expect(res.statusCode).toBe(403); + }); +}); + +describe("GET /session-sharing/resolve/:linkToken", () => { + async function createActiveLinkShare( + overrides: Partial> = {}, + ) { + await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + ...overrides, + }, + }); + const [share] = [...state.shares.values()]; + return share as { linkToken: string; id: string }; + } + + it("never includes hostname, ip, username, or hostId in the response body", async () => { + const share = await createActiveLinkShare(); + + const res = await invoke("get", "/resolve/:linkToken", { + params: { linkToken: share.linkToken }, + }); + + expect(res.statusCode).toBe(200); + const body = res.jsonBody as Record; + const serialized = JSON.stringify(body).toLowerCase(); + + expect(body).not.toHaveProperty("hostname"); + expect(body).not.toHaveProperty("ip"); + expect(body).not.toHaveProperty("username"); + expect(body).not.toHaveProperty("hostId"); + expect(body).not.toHaveProperty("hostName"); + expect(serialized).not.toContain("10.0.0"); + expect(serialized).not.toContain("hostname"); + expect(serialized).not.toContain('"ip"'); + expect(serialized).not.toContain("username"); + }); + + it("returns only protocol/permissionLevel/wsPath(/connectParams) for ssh", async () => { + const share = await createActiveLinkShare(); + + const res = await invoke("get", "/resolve/:linkToken", { + params: { linkToken: share.linkToken }, + }); + + expect(res.jsonBody).toEqual({ + protocol: "ssh", + permissionLevel: "read-only", + wsPath: `/terminal/ws?shareToken=${encodeURIComponent(share.linkToken)}`, + }); + }); + + it("mints a fresh join token for guac protocols", async () => { + await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "guac-conn-1", + protocol: "vnc", + shareType: "link", + permissionLevel: "read-only", + }, + }); + const [share] = [...state.shares.values()] as { + linkToken: string; + }[]; + + const res = await invoke("get", "/resolve/:linkToken", { + params: { linkToken: share.linkToken }, + }); + + expect(res.statusCode).toBe(200); + expect((res.jsonBody as Record).connectParams).toEqual({ + token: "join-token:guac-conn-1:true", + }); + }); + + it("rejects an unknown link token", async () => { + const res = await invoke("get", "/resolve/:linkToken", { + params: { linkToken: "does-not-exist" }, + }); + + expect(res.statusCode).toBe(404); + }); + + it("rejects a revoked link token", async () => { + const share = await createActiveLinkShare(); + await invoke("delete", "/:shareId", { params: { shareId: share.id } }); + + const res = await invoke("get", "/resolve/:linkToken", { + params: { linkToken: share.linkToken }, + }); + + expect(res.statusCode).toBe(404); + }); + + it("rejects an expired link token", async () => { + state.shares.set("share-expired", { + id: "share-expired", + hostId: 1, + ownerUserId: "user-1", + protocol: "ssh", + sessionId: "session-1", + shareType: "link", + linkToken: "expired-token", + permissionLevel: "read-only", + expiresAt: "2000-01-01T00:00:00.000Z", + revokedAt: null, + }); + + const res = await invoke("get", "/resolve/:linkToken", { + params: { linkToken: "expired-token" }, + }); + + expect(res.statusCode).toBe(404); + }); + + it("re-checks the global kill switch at resolve time, not just at creation time", async () => { + const share = await createActiveLinkShare(); + + state.globalSharingEnabled = false; + + const res = await invoke("get", "/resolve/:linkToken", { + params: { linkToken: share.linkToken }, + }); + + expect(res.statusCode).toBe(404); + }); +}); + +describe("DELETE /session-sharing/:shareId", () => { + it("allows the owner to revoke their own share", async () => { + await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + }, + }); + const [share] = [...state.shares.values()] as { id: string }[]; + + const res = await invoke("delete", "/:shareId", { + params: { shareId: share.id }, + }); + + expect(res.statusCode).toBe(200); + }); + + it("rejects a non-owner, non-admin caller", async () => { + await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + }, + }); + const [share] = [...state.shares.values()] as { id: string }[]; + + state.currentUserId = "user-2"; + const res = await invoke("delete", "/:shareId", { + params: { shareId: share.id }, + }); + + expect(res.statusCode).toBe(403); + }); + + it("allows an admin to revoke someone else's share", async () => { + await invoke("post", "/create", { + body: { + hostId: 1, + sessionId: "session-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + }, + }); + const [share] = [...state.shares.values()] as { id: string }[]; + + state.currentUserId = "admin-1"; + state.admins.add("admin-1"); + const res = await invoke("delete", "/:shareId", { + params: { shareId: share.id }, + }); + + expect(res.statusCode).toBe(200); + }); +}); diff --git a/src/backend/tests/hosts/tailscale-check.test.ts b/src/backend/tests/hosts/tailscale-check.test.ts new file mode 100644 index 0000000..7305826 --- /dev/null +++ b/src/backend/tests/hosts/tailscale-check.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from "vitest"; +import { + parseTailscaleCheckBanner, + isTailscaleCheckCompleteBanner, +} from "../../hosts/tailscale-check.js"; + +describe("parseTailscaleCheckBanner", () => { + it("extracts the login URL from a real check-mode banner", () => { + const banner = + "# Tailscale SSH requires an additional check.\n# To authenticate, visit: https://login.tailscale.com/a/lefcb2f3377403\n"; + + const result = parseTailscaleCheckBanner(banner); + + expect(result).not.toBeNull(); + expect(result?.url).toBe("https://login.tailscale.com/a/lefcb2f3377403"); + }); + + it("strips comment markers from the message it returns", () => { + const banner = + "# Tailscale SSH requires an additional check.\n# To authenticate, visit: https://login.tailscale.com/a/abc123\n"; + + const result = parseTailscaleCheckBanner(banner); + + expect(result?.message).toBe( + "Tailscale SSH requires an additional check.\nTo authenticate, visit: https://login.tailscale.com/a/abc123", + ); + }); + + it("returns null for an ordinary MOTD banner", () => { + const banner = + "Welcome to Ubuntu 24.04 LTS\nLast login: Tue Aug 5 09:12:03 2026\n"; + + expect(parseTailscaleCheckBanner(banner)).toBeNull(); + }); + + it("returns null for a lookalike URL on another host", () => { + const banner = + "# To authenticate, visit: https://login.tailscale.com.evil.example/a/abc123\n"; + + expect(parseTailscaleCheckBanner(banner)).toBeNull(); + }); + + it("returns null for empty input", () => { + expect(parseTailscaleCheckBanner("")).toBeNull(); + }); +}); + +describe("isTailscaleCheckCompleteBanner", () => { + it("recognises the completion banner", () => { + expect( + isTailscaleCheckCompleteBanner( + "# Authentication checked with Tailscale SSH.", + ), + ).toBe(true); + }); + + it("recognises the completion banner with a time suffix", () => { + expect( + isTailscaleCheckCompleteBanner( + "Authentication checked with Tailscale SSH. Time since last authentication: 0s", + ), + ).toBe(true); + }); + + it("does not match the check-required banner", () => { + expect( + isTailscaleCheckCompleteBanner( + "# Tailscale SSH requires an additional check.", + ), + ).toBe(false); + }); + + it("does not match empty input", () => { + expect(isTailscaleCheckCompleteBanner("")).toBe(false); + }); +}); diff --git a/src/backend/tests/hosts/terminal/host-identity.test.ts b/src/backend/tests/hosts/terminal/host-identity.test.ts new file mode 100644 index 0000000..d9e1ac4 --- /dev/null +++ b/src/backend/tests/hosts/terminal/host-identity.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from "vitest"; +import { + hostAddressMismatch, + HOST_ADDRESS_MISMATCH_MESSAGE, + HOST_NOT_ON_THIS_SERVER_MESSAGE, + HostAddressMismatchError, + HostNotOnThisServerError, + normalizeHostAddress, +} from "../../../hosts/terminal/host-identity.js"; + +/** + * The desktop app lists hosts out of its own embedded database and identifies + * them to the backend by numeric row id. With the connection origin set to + * "Remote server" that id is resolved against the sync server's `ssh_data` + * instead, whose autoincrement ids drift apart from the client's as soon as + * the two sides accumulate inserts and deletes in a different order. + * + * The resolved row supplies the address, the credentials, the jump hosts and + * the stored host key, so the session opened on whichever machine owned that + * id on the server โ€” the host list stayed correct the whole time, and nothing + * announced the substitution. + */ +describe("hostAddressMismatch", () => { + it("refuses an id that resolves to a different machine", () => { + expect(hostAddressMismatch("10.0.0.7", "10.0.0.9")).toBe(true); + expect(hostAddressMismatch("aeza.example.com", "rpi.example.com")).toBe( + true, + ); + }); + + it("allows the ordinary case where both sides agree", () => { + expect(hostAddressMismatch("10.0.0.7", "10.0.0.7")).toBe(false); + }); + + it("does not trip over how an address is written", () => { + // The client strips brackets off IPv6 literals before connecting; the + // stored row keeps them. Same machine either way. + expect(hostAddressMismatch("2001:db8::1", "[2001:db8::1]")).toBe(false); + expect(hostAddressMismatch("Host.Example.COM", "host.example.com")).toBe( + false, + ); + expect(hostAddressMismatch("10.0.0.7", " 10.0.0.7 ")).toBe(false); + }); + + it("stays out of the way when the server has no address to compare", () => { + // Nothing stored server-side: the caller falls back to what the client + // supplied, as it always has. Refusing here would break every setup that + // passes host details inline. + expect(hostAddressMismatch("10.0.0.7", undefined)).toBe(false); + expect(hostAddressMismatch("10.0.0.7", null)).toBe(false); + expect(hostAddressMismatch("10.0.0.7", "")).toBe(false); + expect(hostAddressMismatch("10.0.0.7", " ")).toBe(false); + }); + + it("refuses when the client sent nothing but the server resolved a host", () => { + // An id alone must not be enough to pick a machine. + expect(hostAddressMismatch(undefined, "10.0.0.9")).toBe(true); + expect(hostAddressMismatch("", "10.0.0.9")).toBe(true); + }); +}); + +describe("HostAddressMismatchError", () => { + it("survives the catch blocks that swallow resolution failures", () => { + // SFTP host resolution sits inside "failed to resolve credentials, carry + // on" handlers. Continuing is precisely what must not happen here, so + // those catches rethrow this type -- which only works if it is + // recognisable with instanceof after being thrown. + const rethrow = () => { + try { + throw new HostAddressMismatchError(); + } catch (error) { + if (error instanceof HostAddressMismatchError) throw error; + return "swallowed"; + } + }; + + expect(rethrow).toThrow(HostAddressMismatchError); + expect(rethrow).toThrow(HOST_ADDRESS_MISMATCH_MESSAGE); + }); + + it("tells the user which of their settings to change", () => { + // The message is the only actionable thing they get; the workaround has + // to be in it. + expect(HOST_ADDRESS_MISMATCH_MESSAGE).toContain("This device"); + expect(HOST_ADDRESS_MISMATCH_MESSAGE).toContain("full sync"); + }); +}); + +describe("HostNotOnThisServerError", () => { + it("is distinguishable from a mismatch, and survives a rethrow", () => { + // Different remedies: an unknown host needs syncing across, a mismatched + // one needs a different origin. The SFTP catches rethrow both. + const thrown = (() => { + try { + throw new HostNotOnThisServerError(); + } catch (error) { + return error; + } + })(); + + expect(thrown).toBeInstanceOf(HostNotOnThisServerError); + expect(thrown).not.toBeInstanceOf(HostAddressMismatchError); + expect((thrown as Error).message).toBe(HOST_NOT_ON_THIS_SERVER_MESSAGE); + }); + + it("tells the user to sync rather than to switch origin", () => { + expect(HOST_NOT_ON_THIS_SERVER_MESSAGE).toContain("sync"); + expect(HOST_NOT_ON_THIS_SERVER_MESSAGE).toContain("This device"); + }); +}); + +describe("normalizeHostAddress", () => { + it("keeps only what identifies the host", () => { + expect(normalizeHostAddress("[2001:db8::1]")).toBe("2001:db8::1"); + expect(normalizeHostAddress(" Example.COM ")).toBe("example.com"); + }); + + it("treats anything that is not a string as no address", () => { + expect(normalizeHostAddress(undefined)).toBe(""); + expect(normalizeHostAddress(null)).toBe(""); + expect(normalizeHostAddress(42)).toBe(""); + }); +}); diff --git a/src/backend/tests/hosts/terminal/session-manager.test.ts b/src/backend/tests/hosts/terminal/session-manager.test.ts index 9a36971..5b94ed1 100644 --- a/src/backend/tests/hosts/terminal/session-manager.test.ts +++ b/src/backend/tests/hosts/terminal/session-manager.test.ts @@ -49,9 +49,19 @@ vi.mock("fs", () => ({ }, })); -const { sessionManager } = +const { sessionManager, isMessageAllowedForParticipant } = await import("../../../hosts/terminal/session-manager.js"); +// Minimal fake WebSocket - only the surface session-manager touches. +function makeFakeWs(readyState = 1 /* OPEN */) { + return { + readyState, + send: vi.fn(), + } as unknown as import("ws").WebSocket; +} +const WS_OPEN = 1; +const WS_CLOSED = 3; + describe("TerminalSessionManager - session logging", () => { beforeEach(() => { vi.clearAllMocks(); @@ -150,3 +160,273 @@ describe("TerminalSessionManager - session logging", () => { sessionManager.destroySession(id); }); }); + +describe("TerminalSessionManager - multiplayer participants", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockMkdir.mockResolvedValue(undefined); + mockWriteFile.mockResolvedValue(undefined); + mockCreate.mockResolvedValue({ id: 1 }); + mockUpdateEnded.mockResolvedValue(undefined); + }); + + function createConnectedSession(): string { + const id = sessionManager.createSession( + "owner-1", + 1, + "host", + 80, + 24, + undefined, + false, + ); + // Mark connected without a real ssh2 stream - only isConnected is read + // by attachWs/joinAsParticipant. + const session = sessionManager.getSession(id)!; + session.isConnected = true; + return id; + } + + it("joinAsParticipant adds a participant without evicting the owner", () => { + const id = createConnectedSession(); + const ownerWs = makeFakeWs(); + sessionManager.attachWs(id, "owner-1", ownerWs); + + const guestWs = makeFakeWs(); + const session = sessionManager.joinAsParticipant(id, guestWs, { + userId: null, + permissionLevel: "read-only", + guestLabel: "Guest", + }); + + expect(session).not.toBeNull(); + expect(session!.participants.size).toBe(2); + const ownerParticipant = sessionManager.getParticipantForWs( + session!, + ownerWs, + ); + expect(ownerParticipant?.isOwner).toBe(true); + expect(ownerWs.send).not.toHaveBeenCalled(); + + sessionManager.destroySession(id); + }); + + it("joinAsParticipant returns null for a nonexistent or unconnected session", () => { + expect( + sessionManager.joinAsParticipant("does-not-exist", makeFakeWs(), { + userId: null, + permissionLevel: "read-only", + }), + ).toBeNull(); + }); + + it("broadcast sends to all OPEN participant sockets and skips CLOSED ones", () => { + const id = createConnectedSession(); + const ownerWs = makeFakeWs(WS_OPEN); + sessionManager.attachWs(id, "owner-1", ownerWs); + + const openGuestWs = makeFakeWs(WS_OPEN); + const closedGuestWs = makeFakeWs(WS_CLOSED); + sessionManager.joinAsParticipant(id, openGuestWs, { + userId: null, + permissionLevel: "read-only", + }); + sessionManager.joinAsParticipant(id, closedGuestWs, { + userId: null, + permissionLevel: "read-only", + }); + + sessionManager.broadcast(id, { type: "data", data: "hello" }); + + expect(ownerWs.send).toHaveBeenCalledWith( + JSON.stringify({ type: "data", data: "hello" }), + ); + expect(openGuestWs.send).toHaveBeenCalledWith( + JSON.stringify({ type: "data", data: "hello" }), + ); + expect(closedGuestWs.send).not.toHaveBeenCalled(); + + sessionManager.destroySession(id); + }); + + it("broadcast does not throw if a socket's send throws", () => { + const id = createConnectedSession(); + const throwingWs = makeFakeWs(WS_OPEN); + (throwingWs.send as ReturnType).mockImplementation(() => { + throw new Error("send failed"); + }); + sessionManager.attachWs(id, "owner-1", throwingWs); + + expect(() => + sessionManager.broadcast(id, { type: "data", data: "x" }), + ).not.toThrow(); + + sessionManager.destroySession(id); + }); + + it("broadcast is a no-op for a nonexistent session", () => { + expect(() => + sessionManager.broadcast("does-not-exist", { type: "data" }), + ).not.toThrow(); + }); + + it("owner detach arms the idle timeout (existing behavior)", () => { + vi.useFakeTimers(); + try { + const id = createConnectedSession(); + const ownerWs = makeFakeWs(); + sessionManager.attachWs(id, "owner-1", ownerWs); + + sessionManager.detachWs(id); + const session = sessionManager.getSession(id); + expect(session?.detachTimeout).not.toBeNull(); + expect(session?.lastDetachedAt).not.toBeNull(); + + sessionManager.destroySession(id); + } finally { + vi.useRealTimers(); + } + }); + + it("removeParticipant on a non-owner does not arm a timeout or destroy the session", () => { + const id = createConnectedSession(); + const ownerWs = makeFakeWs(); + sessionManager.attachWs(id, "owner-1", ownerWs); + + const guestWs = makeFakeWs(); + sessionManager.joinAsParticipant(id, guestWs, { + userId: null, + permissionLevel: "read-write", + }); + + sessionManager.removeParticipant(id, guestWs); + + const session = sessionManager.getSession(id); + expect(session).not.toBeNull(); + expect(session?.detachTimeout).toBeNull(); + expect(session?.participants.size).toBe(1); + expect(sessionManager.getParticipantForWs(session!, guestWs)).toBeNull(); + + sessionManager.destroySession(id); + }); + + it("removeParticipant is a no-op when the ws belongs to the owner", () => { + const id = createConnectedSession(); + const ownerWs = makeFakeWs(); + sessionManager.attachWs(id, "owner-1", ownerWs); + + sessionManager.removeParticipant(id, ownerWs); + + const session = sessionManager.getSession(id); + expect(session?.participants.size).toBe(1); + expect(sessionManager.getParticipantForWs(session!, ownerWs)?.isOwner).toBe( + true, + ); + + sessionManager.destroySession(id); + }); + + it("destroySession cleans up all participants, not just the owner", () => { + const id = createConnectedSession(); + const ownerWs = makeFakeWs(); + sessionManager.attachWs(id, "owner-1", ownerWs); + + const guestWs = makeFakeWs(); + sessionManager.joinAsParticipant(id, guestWs, { + userId: null, + permissionLevel: "read-only", + }); + + sessionManager.destroySession(id); + + expect(guestWs.send).toHaveBeenCalled(); + expect(sessionManager.getSession(id)).toBeNull(); + }); + + it("ownerEndSession notifies non-owner participants and destroys the session", () => { + const id = createConnectedSession(); + const ownerWs = makeFakeWs(); + sessionManager.attachWs(id, "owner-1", ownerWs); + + const guestWs = makeFakeWs(); + sessionManager.joinAsParticipant(id, guestWs, { + userId: null, + permissionLevel: "read-write", + }); + + sessionManager.ownerEndSession(id, "owner ended the session"); + + expect(guestWs.send).toHaveBeenCalledWith( + JSON.stringify({ + type: "sessionTerminatedByOwner", + reason: "owner ended the session", + }), + ); + expect(sessionManager.getSession(id)).toBeNull(); + }); +}); + +describe("isMessageAllowedForParticipant", () => { + it("allows any message type for the owner or when there is no participant", () => { + expect(isMessageAllowedForParticipant(null, "connectToHost")).toBe(true); + expect( + isMessageAllowedForParticipant( + { isOwner: true, permissionLevel: "read-write" }, + "resize", + ), + ).toBe(true); + }); + + it("drops input from a read-only participant", () => { + expect( + isMessageAllowedForParticipant( + { isOwner: false, permissionLevel: "read-only" }, + "input", + ), + ).toBe(false); + }); + + it("allows input from a read-write non-owner participant", () => { + expect( + isMessageAllowedForParticipant( + { isOwner: false, permissionLevel: "read-write" }, + "input", + ), + ).toBe(true); + }); + + it("allows ping and disconnect for any non-owner participant", () => { + expect( + isMessageAllowedForParticipant( + { isOwner: false, permissionLevel: "read-only" }, + "ping", + ), + ).toBe(true); + expect( + isMessageAllowedForParticipant( + { isOwner: false, permissionLevel: "read-only" }, + "disconnect", + ), + ).toBe(true); + }); + + it("blocks resize and auth/tmux message types for non-owner participants regardless of permission level", () => { + for (const type of [ + "resize", + "totp_response", + "password_response", + "tmux_attach", + "tmux_detach", + "get_cwd", + "vault_start_auth", + "opkssh_start_auth", + ]) { + expect( + isMessageAllowedForParticipant( + { isOwner: false, permissionLevel: "read-write" }, + type, + ), + ).toBe(false); + } + }); +}); diff --git a/src/backend/tests/hosts/tmux/auth-utils.test.ts b/src/backend/tests/hosts/tmux/auth-utils.test.ts new file mode 100644 index 0000000..23488c8 --- /dev/null +++ b/src/backend/tests/hosts/tmux/auth-utils.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { getTmuxAuthBehavior } from "../../../hosts/tmux/auth-utils.js"; + +describe("getTmuxAuthBehavior", () => { + it("uses credentialless non-interactive authentication for Tailscale SSH", () => { + expect(getTmuxAuthBehavior("tailscale")).toEqual({ + credentialless: true, + tryKeyboard: false, + }); + }); + + it("preserves keyboard-interactive fallback for none authentication", () => { + expect(getTmuxAuthBehavior("none")).toEqual({ + credentialless: true, + tryKeyboard: true, + }); + }); + + it("does not treat password authentication as credentialless", () => { + expect(getTmuxAuthBehavior("password")).toEqual({ + credentialless: false, + tryKeyboard: true, + }); + }); +}); diff --git a/src/backend/tests/hosts/tmux/helper.test.ts b/src/backend/tests/hosts/tmux/helper.test.ts index ccd60ac..33e168e 100644 --- a/src/backend/tests/hosts/tmux/helper.test.ts +++ b/src/backend/tests/hosts/tmux/helper.test.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "node:events"; +import { execFileSync } from "node:child_process"; import type { Client } from "ssh2"; import { describe, expect, it } from "vitest"; import { @@ -8,24 +9,42 @@ import { } from "../../../hosts/tmux/helper.js"; describe("tmux command path handling", () => { - it("adds common non-login shell tmux paths", () => { - const command = withTmuxPath("command -v tmux"); - - expect(command).toMatch(/^\/bin\/sh -c '/); - expect(command).toContain("/opt/homebrew/bin"); - expect(command).toContain("/usr/local/bin"); - expect(command).toContain("/opt/bin"); - expect(command).toContain("/usr/pkg/bin"); - expect(command).toContain(":$PATH; export PATH; command -v tmux"); - }); - - it("wraps tmux invocations with the same path", () => { - expect(tmuxCommand("list-sessions")).toMatch( - /^\/bin\/sh -c 'PATH=.*:\$PATH; export PATH; tmux list-sessions'$/, + it("prepends all non-login tmux paths while preserving inherited PATH", () => { + expect(withTmuxPath("command -v tmux")).toBe( + `/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; command -v tmux'`, ); }); - it("detects suffixed tmux versions without parsing the version number", async () => { + it("shell-escapes embedded single quotes in wrapped commands", () => { + // Asserted as a string so the escaping rule is covered everywhere. The + // round-trip below proves it against a real parser, but only where one + // exists -- see the note there. + expect(withTmuxPath(`printf '%s' "can't"`)).toBe( + "/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:\"$PATH\"; export PATH; printf '\\''%s'\\'' \"can'\\''t\"'", + ); + }); + + // /bin/sh is not on Windows, and Windows is a supported platform for the + // desktop app -- contributors run `npm test` there. CI is ubuntu-only, so it + // would never notice this failing. + it.skipIf(process.platform === "win32")( + "produces a command a real shell parses back to the original", + () => { + const command = withTmuxPath(`printf '%s' "can't"`); + + expect( + execFileSync("/bin/sh", ["-c", command], { encoding: "utf8" }), + ).toBe("can't"); + }, + ); + + it("runs every tmux invocation in UTF-8 mode through the path wrapper", () => { + expect(tmuxCommand("list-sessions")).toBe( + `/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; tmux -u list-sessions'`, + ); + }); + + it("detects tmux with the UTF-8 wrapper", async () => { const commands: string[] = []; const conn = { exec(command: string, callback: (error: null, stream: never) => void) { @@ -51,6 +70,9 @@ describe("tmux command path handling", () => { available: true, sessions: [], }); - expect(commands[0]).toContain("tmux -V"); + expect(commands).toEqual([ + `/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; tmux -u -V'`, + `/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; tmux -u list-sessions -F "#{session_name}|#{session_created}|#{session_activity}|#{session_windows}|#{session_attached}" 2>/dev/null'`, + ]); }); }); diff --git a/src/backend/tests/utils/alert-trigger.test.ts b/src/backend/tests/utils/alert-trigger.test.ts new file mode 100644 index 0000000..1edf42d --- /dev/null +++ b/src/backend/tests/utils/alert-trigger.test.ts @@ -0,0 +1,94 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { triggerLoginAlert } from "../../utils/alert-trigger.js"; +import { SystemCrypto } from "../../utils/system-crypto.js"; +import { sshLogger } from "../../utils/logger.js"; + +describe("triggerLoginAlert", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("reports a rejected metrics-service request", async () => { + vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({ + getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"), + } as never); + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response('{"error":"Missing authentication token"}', { + status: 401, + }), + ); + const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {}); + + await triggerLoginAlert(7, "user-1", "root", "192.0.2.1"); + + expect(warn).toHaveBeenCalledWith( + "Failed to trigger login alert", + expect.objectContaining({ + operation: "login_alert_trigger_error", + hostId: 7, + error: + 'Metrics service returned 401: {"error":"Missing authentication token"}', + }), + ); + }); + + it("does not log a warning when the metrics service accepts the event", async () => { + vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({ + getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"), + } as never); + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response('{"ok":true}', { status: 200 }), + ); + const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {}); + + await triggerLoginAlert(7, "user-1", "root", "192.0.2.1"); + + expect(warn).not.toHaveBeenCalled(); + }); + + it("sends the internal auth token and login details the metrics service expects", async () => { + vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({ + getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"), + } as never); + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response('{"ok":true}', { status: 200 })); + + await triggerLoginAlert(42, "user-1", "root", "10.0.0.5"); + + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:30005/internal/login-alert", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "x-internal-auth": "internal-token", + }), + body: JSON.stringify({ + hostId: 42, + userId: "user-1", + sshUser: "root", + fromIp: "10.0.0.5", + }), + }), + ); + }); + + it("logs a warning if the fetch itself throws, instead of propagating", async () => { + vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({ + getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"), + } as never); + vi.spyOn(globalThis, "fetch").mockRejectedValue( + new Error("connect ECONNREFUSED"), + ); + const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {}); + + await expect( + triggerLoginAlert(1, "user-1", "root", "127.0.0.1"), + ).resolves.toBeUndefined(); + + expect(warn).toHaveBeenCalledWith( + "Failed to trigger login alert", + expect.objectContaining({ hostId: 1 }), + ); + }); +}); diff --git a/src/backend/tests/utils/analytics.test.ts b/src/backend/tests/utils/analytics.test.ts new file mode 100644 index 0000000..a121051 --- /dev/null +++ b/src/backend/tests/utils/analytics.test.ts @@ -0,0 +1,193 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +const mockGetBoolean = vi.fn(); +const mockGet = vi.fn(); +const mockSet = vi.fn(); +const mockPost = vi.fn(); + +function makeChain(resolveValue: unknown) { + const chain: Record = {}; + const methods = ["from", "where", "groupBy"]; + for (const m of methods) { + chain[m] = vi.fn(() => chain); + } + (chain as unknown as Promise).then = (cb: (v: unknown) => unknown) => + Promise.resolve(resolveValue).then(cb); + return chain; +} + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentSettingsRepository: () => ({ + getBoolean: mockGetBoolean, + get: mockGet, + set: mockSet, + }), + createCurrentRepositoryContext: () => ({ + drizzle: { + select: vi.fn(() => makeChain([{ count: 0 }])), + }, + }), +})); + +vi.mock("../../database/db/schema.js", () => ({ + users: {}, + hosts: {}, + recentActivity: { type: "type", timestamp: "timestamp" }, +})); + +vi.mock("../../utils/logger.js", () => ({ + Logger: class { + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + }, +})); + +vi.mock("axios", () => ({ + default: { post: mockPost }, +})); + +describe("analytics", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + vi.clearAllMocks(); + process.env = { ...originalEnv }; + delete process.env.ENABLE_TELEMETRY; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it("isAnalyticsEnabled defaults to true via the settings repository", async () => { + delete process.env.ENABLE_TELEMETRY; + mockGetBoolean.mockResolvedValue(true); + const { isAnalyticsEnabled } = await import("../../utils/analytics.js"); + + const result = await isAnalyticsEnabled(); + + expect(result).toBe(true); + expect(mockGetBoolean).toHaveBeenCalledWith("analytics_enabled", true); + }); + + it("ENABLE_TELEMETRY=false disables analytics without consulting the database", async () => { + process.env.ENABLE_TELEMETRY = "false"; + const { isAnalyticsEnabled } = await import("../../utils/analytics.js"); + + const result = await isAnalyticsEnabled(); + + expect(result).toBe(false); + expect(mockGetBoolean).not.toHaveBeenCalled(); + }); + + it("ENABLE_TELEMETRY=true forces analytics on without consulting the database", async () => { + process.env.ENABLE_TELEMETRY = "TRUE"; + const { isAnalyticsEnabled } = await import("../../utils/analytics.js"); + + const result = await isAnalyticsEnabled(); + + expect(result).toBe(true); + expect(mockGetBoolean).not.toHaveBeenCalled(); + }); + + it("getTelemetryEnvOverride returns null when unset or blank", async () => { + const { getTelemetryEnvOverride } = + await import("../../utils/analytics.js"); + + delete process.env.ENABLE_TELEMETRY; + expect(getTelemetryEnvOverride()).toBe(null); + + process.env.ENABLE_TELEMETRY = " "; + expect(getTelemetryEnvOverride()).toBe(null); + }); + + it("startAnalyticsHeartbeat sends nothing when ENABLE_TELEMETRY=false", async () => { + process.env.ENABLE_TELEMETRY = "false"; + process.env.POSTHOG_API_KEY = "phc_test"; + const { startAnalyticsHeartbeat } = + await import("../../utils/analytics.js"); + + startAnalyticsHeartbeat(); + await Promise.resolve(); + + expect(mockPost).not.toHaveBeenCalled(); + }); + + it("collectAndSendHeartbeat does not call PostHog when ENABLE_TELEMETRY=false", async () => { + process.env.ENABLE_TELEMETRY = "false"; + process.env.POSTHOG_API_KEY = "phc_test"; + const { collectAndSendHeartbeat } = + await import("../../utils/analytics.js"); + + await collectAndSendHeartbeat(); + + expect(mockPost).not.toHaveBeenCalled(); + }); + + it("getOrCreateInstanceId returns the existing id without generating one", async () => { + mockGet.mockResolvedValue("existing-id"); + const { getOrCreateInstanceId } = await import("../../utils/analytics.js"); + + const id = await getOrCreateInstanceId(); + + expect(id).toBe("existing-id"); + expect(mockSet).not.toHaveBeenCalled(); + }); + + it("getOrCreateInstanceId generates and persists a new id when absent", async () => { + mockGet.mockResolvedValue(null); + const { getOrCreateInstanceId } = await import("../../utils/analytics.js"); + + const id = await getOrCreateInstanceId(); + + expect(id).toMatch(/^[0-9a-f-]{36}$/); + expect(mockSet).toHaveBeenCalledWith("analytics_instance_id", id); + }); + + it("collectAndSendHeartbeat does not call PostHog when POSTHOG_API_KEY is unset", async () => { + delete process.env.POSTHOG_API_KEY; + const { collectAndSendHeartbeat } = + await import("../../utils/analytics.js"); + + await collectAndSendHeartbeat(); + + expect(mockPost).not.toHaveBeenCalled(); + }); + + it("collectAndSendHeartbeat does not call PostHog when analytics is disabled", async () => { + process.env.POSTHOG_API_KEY = "phc_test"; + mockGetBoolean.mockResolvedValue(false); + const { collectAndSendHeartbeat } = + await import("../../utils/analytics.js"); + + await collectAndSendHeartbeat(); + + expect(mockPost).not.toHaveBeenCalled(); + }); + + it("collectAndSendHeartbeat posts a heartbeat event with the expected shape when enabled", async () => { + process.env.POSTHOG_API_KEY = "phc_test"; + mockGetBoolean.mockResolvedValue(true); + mockGet.mockResolvedValue("instance-123"); + mockPost.mockResolvedValue({}); + const { collectAndSendHeartbeat } = + await import("../../utils/analytics.js"); + + await collectAndSendHeartbeat(); + + expect(mockPost).toHaveBeenCalledTimes(1); + const [url, body] = mockPost.mock.calls[0]; + expect(url).toContain("/capture/"); + expect(body).toMatchObject({ + api_key: "phc_test", + event: "instance_heartbeat", + distinct_id: "instance-123", + properties: expect.objectContaining({ + user_count: 0, + host_count: 0, + used_terminal: 0, + }), + }); + }); +}); diff --git a/src/backend/tests/utils/audit-export.test.ts b/src/backend/tests/utils/audit-export.test.ts new file mode 100644 index 0000000..9e31342 --- /dev/null +++ b/src/backend/tests/utils/audit-export.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from "vitest"; +import { + escapeCsvField, + exportFilename, + toCsv, + toNdjson, +} from "../../utils/audit-export.js"; +import type { AuditLogRecord } from "../../database/repositories/audit-log-repository.js"; + +function entry(overrides: Partial = {}): AuditLogRecord { + return { + id: 1, + userId: "u-1", + username: "alice", + action: "delete_host", + resourceType: "host", + resourceId: "9", + resourceName: "prod-db", + details: null, + ipAddress: "203.0.113.9", + userAgent: "Mozilla/5.0", + success: true, + errorMessage: null, + timestamp: "2026-07-28 10:00:00", + ...overrides, + } as AuditLogRecord; +} + +describe("escapeCsvField", () => { + it("leaves plain values alone", () => { + expect(escapeCsvField("prod-db")).toBe("prod-db"); + expect(escapeCsvField(42)).toBe("42"); + expect(escapeCsvField(true)).toBe("true"); + }); + + it("renders null and undefined as empty", () => { + expect(escapeCsvField(null)).toBe(""); + expect(escapeCsvField(undefined)).toBe(""); + }); + + it("quotes and doubles embedded quotes", () => { + expect(escapeCsvField('say "hi"')).toBe('"say ""hi"""'); + }); + + it("quotes values containing commas or newlines", () => { + expect(escapeCsvField("a,b")).toBe('"a,b"'); + expect(escapeCsvField("line1\nline2")).toBe('"line1\nline2"'); + }); + + it("neutralises spreadsheet formulas", () => { + // An audit entry can carry an attacker-chosen resource name; without this + // the exported file executes it when opened. + expect(escapeCsvField("=1+1")).toBe("'=1+1"); + expect(escapeCsvField("+SUM(A1)")).toBe("'+SUM(A1)"); + expect(escapeCsvField("-2+3")).toBe("'-2+3"); + expect(escapeCsvField("@import")).toBe("'@import"); + }); + + it("still quotes a formula that also contains a comma", () => { + expect(escapeCsvField("=A1,B2")).toBe(`"'=A1,B2"`); + }); +}); + +describe("toCsv", () => { + it("writes a header even with no rows", () => { + expect(toCsv([])).toBe( + "id,timestamp,username,userId,action,resourceType,resourceId,resourceName,success,ipAddress,userAgent,errorMessage,details\n", + ); + }); + + it("writes one line per entry in column order", () => { + const lines = toCsv([entry(), entry({ id: 2, username: "bob" })]) + .trim() + .split("\n"); + + expect(lines).toHaveLength(3); + expect( + lines[1].startsWith("1,2026-07-28 10:00:00,alice,u-1,delete_host"), + ).toBe(true); + expect(lines[2].startsWith("2,")).toBe(true); + }); + + it("keeps a detached entry readable", () => { + const line = toCsv([entry({ userId: null })]) + .trim() + .split("\n")[1]; + + // username survives so the row still names who acted. + expect(line).toContain("alice"); + expect(line.split(",")[3]).toBe(""); + }); +}); + +describe("toNdjson", () => { + it("emits one parseable object per line", () => { + const out = toNdjson([entry(), entry({ id: 2 })]); + const parsed = out + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + expect(parsed).toHaveLength(2); + expect(parsed[0].action).toBe("delete_host"); + expect(parsed[1].id).toBe(2); + }); + + it("returns nothing for an empty set", () => { + expect(toNdjson([])).toBe(""); + }); +}); + +describe("exportFilename", () => { + it("is filesystem-safe and carries the timestamp", () => { + const name = exportFilename("csv", new Date("2026-07-28T10:11:12.000Z")); + + expect(name).toBe("termix-audit-2026-07-28-10-11-12.csv"); + expect(name).not.toMatch(/[:\s]/); + }); + + it("uses the ndjson extension for the streaming format", () => { + expect(exportFilename("ndjson", new Date("2026-07-28T10:11:12.000Z"))).toBe( + "termix-audit-2026-07-28-10-11-12.ndjson", + ); + }); +}); diff --git a/src/backend/tests/utils/audit-forwarder.test.ts b/src/backend/tests/utils/audit-forwarder.test.ts new file mode 100644 index 0000000..fdfa607 --- /dev/null +++ b/src/backend/tests/utils/audit-forwarder.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const safeFetch = vi.hoisted(() => vi.fn()); +const logs = vi.hoisted(() => ({ info: vi.fn(), warn: vi.fn() })); + +vi.mock("../../utils/safe-outbound-fetch.js", () => ({ + safeOutboundFetch: safeFetch, +})); +vi.mock("../../utils/logger.js", () => ({ databaseLogger: logs })); + +const { + auditForwardTarget, + forwardAuditEntry, + forwardPayload, + resetAuditForwarderState, + AUDIT_FORWARD_URL_ENV, + AUDIT_FORWARD_TOKEN_ENV, +} = await import("../../utils/audit-forwarder.js"); + +const ENTRY = { + userId: "u-1", + username: "alice", + action: "delete_host", + resourceType: "host", + resourceId: "9", + success: true, + ipAddress: "203.0.113.9", +}; + +const NOW = new Date("2026-07-28T10:00:00.000Z"); + +beforeEach(() => { + safeFetch.mockReset(); + logs.info.mockReset(); + logs.warn.mockReset(); + resetAuditForwarderState(); +}); + +describe("auditForwardTarget", () => { + it("is off unless a URL is configured", () => { + expect(auditForwardTarget({})).toBeNull(); + expect(auditForwardTarget({ [AUDIT_FORWARD_URL_ENV]: " " })).toBeNull(); + }); + + it("carries an optional bearer token", () => { + expect( + auditForwardTarget({ [AUDIT_FORWARD_URL_ENV]: "https://siem/ingest" }), + ).toEqual({ url: "https://siem/ingest" }); + + expect( + auditForwardTarget({ + [AUDIT_FORWARD_URL_ENV]: "https://siem/ingest", + [AUDIT_FORWARD_TOKEN_ENV]: "secret", + }), + ).toEqual({ url: "https://siem/ingest", token: "secret" }); + }); +}); + +describe("forwardPayload", () => { + it("matches the export shape, with absent fields as null", () => { + expect(forwardPayload(ENTRY, NOW)).toEqual({ + timestamp: "2026-07-28T10:00:00.000Z", + userId: "u-1", + username: "alice", + action: "delete_host", + resourceType: "host", + resourceId: "9", + resourceName: null, + success: true, + ipAddress: "203.0.113.9", + userAgent: null, + errorMessage: null, + details: null, + }); + }); +}); + +describe("forwardAuditEntry", () => { + const env = { [AUDIT_FORWARD_URL_ENV]: "https://siem.example/ingest" }; + + it("does nothing when forwarding is not configured", async () => { + await expect(forwardAuditEntry(ENTRY, NOW, {})).resolves.toBe(false); + expect(safeFetch).not.toHaveBeenCalled(); + }); + + it("posts one NDJSON line through the SSRF-checked fetch", async () => { + safeFetch.mockResolvedValueOnce({ ok: true, status: 200 }); + + await expect(forwardAuditEntry(ENTRY, NOW, env)).resolves.toBe(true); + + const [url, init] = safeFetch.mock.calls[0]; + expect(url).toBe("https://siem.example/ingest"); + expect(init.method).toBe("POST"); + expect(init.headers["Content-Type"]).toBe("application/x-ndjson"); + expect(init.headers.Authorization).toBeUndefined(); + expect(JSON.parse(init.body.trim()).action).toBe("delete_host"); + expect(init.body.endsWith("\n")).toBe(true); + }); + + it("sends the bearer token when one is set", async () => { + safeFetch.mockResolvedValueOnce({ ok: true, status: 200 }); + + await forwardAuditEntry(ENTRY, NOW, { + ...env, + [AUDIT_FORWARD_TOKEN_ENV]: "secret", + }); + + expect(safeFetch.mock.calls[0][1].headers.Authorization).toBe( + "Bearer secret", + ); + }); + + it("reports a rejected delivery without throwing", async () => { + safeFetch.mockResolvedValueOnce({ ok: false, status: 503 }); + + await expect(forwardAuditEntry(ENTRY, NOW, env)).resolves.toBe(false); + expect(logs.warn).toHaveBeenCalledWith( + "Failed to forward audit entry", + expect.objectContaining({ reason: "collector returned 503" }), + ); + }); + + it("swallows transport errors โ€” a dead SIEM must not break auditing", async () => { + safeFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); + + await expect(forwardAuditEntry(ENTRY, NOW, env)).resolves.toBe(false); + expect(logs.warn).toHaveBeenCalledWith( + "Failed to forward audit entry", + expect.objectContaining({ reason: "ECONNREFUSED" }), + ); + }); + + it("stops repeating itself once the collector is persistently down", async () => { + safeFetch.mockResolvedValue({ ok: false, status: 500 }); + + for (let i = 0; i < 8; i++) { + await forwardAuditEntry(ENTRY, NOW, env); + } + + // 5 per-entry warnings, then one suppression notice โ€” not 8. + const perEntry = logs.warn.mock.calls.filter( + (call) => call[0] === "Failed to forward audit entry", + ); + expect(perEntry).toHaveLength(5); + expect( + logs.warn.mock.calls.some((call) => + String(call[0]).includes("suppressing further messages"), + ), + ).toBe(true); + // It keeps trying regardless. + expect(safeFetch).toHaveBeenCalledTimes(8); + }); + + it("announces recovery after a suppressed outage", async () => { + safeFetch.mockResolvedValue({ ok: false, status: 500 }); + for (let i = 0; i < 6; i++) await forwardAuditEntry(ENTRY, NOW, env); + + safeFetch.mockResolvedValueOnce({ ok: true, status: 200 }); + await forwardAuditEntry(ENTRY, NOW, env); + + expect(logs.info).toHaveBeenCalledWith( + "Audit forwarding recovered", + expect.objectContaining({ operation: "audit_forward_recovered" }), + ); + }); +}); diff --git a/src/backend/tests/utils/audit-logger.test.ts b/src/backend/tests/utils/audit-logger.test.ts index b2c9217..08282b0 100644 --- a/src/backend/tests/utils/audit-logger.test.ts +++ b/src/backend/tests/utils/audit-logger.test.ts @@ -68,6 +68,7 @@ describe("getRequestMeta", () => { "user-agent": "TestAgent/1.0", }, ip: "127.0.0.1", + socket: {}, }; const meta = getRequestMeta(req as never); expect(meta.ipAddress).toBe("10.0.0.1"); @@ -78,8 +79,39 @@ describe("getRequestMeta", () => { const req = { headers: { "user-agent": "Bot/2" }, ip: "192.168.1.1", + socket: {}, }; const meta = getRequestMeta(req as never); expect(meta.ipAddress).toBe("192.168.1.1"); }); + + it("splits and trims a forwarded header sent as an array", () => { + const req = { + headers: { + "x-forwarded-for": ["10.0.0.1, 10.0.0.2"], + "user-agent": "TestAgent/1.0", + }, + socket: {}, + }; + const meta = getRequestMeta(req as never); + expect(meta.ipAddress).toBe("10.0.0.1"); + }); + + it("falls back to the socket peer when there is no forwarded header or req.ip", () => { + const req = { + headers: {}, + socket: { remoteAddress: "203.0.113.9" }, + }; + const meta = getRequestMeta(req as never); + expect(meta.ipAddress).toBe("203.0.113.9"); + }); + + it("returns 'unknown' rather than an empty string when no IP info exists", () => { + const req = { + headers: {}, + socket: {}, + }; + const meta = getRequestMeta(req as never); + expect(meta.ipAddress).toBe("unknown"); + }); }); diff --git a/src/backend/tests/utils/audit-retention-migration.test.ts b/src/backend/tests/utils/audit-retention-migration.test.ts new file mode 100644 index 0000000..a4c4c47 --- /dev/null +++ b/src/backend/tests/utils/audit-retention-migration.test.ts @@ -0,0 +1,190 @@ +import { afterEach, describe, expect, it } from "vitest"; +import Database from "better-sqlite3"; +import { + migrateAuditRetention, + userDeleteIsDestructive, +} from "../../utils/audit-retention-migration.js"; + +let db: Database.Database | null = null; + +afterEach(() => { + db?.close(); + db = null; +}); + +/** The pre-migration shape: both tables cascade from users. */ +function legacyDatabase(): Database.Database { + const sqlite = new Database(":memory:"); + sqlite.exec(` + PRAGMA foreign_keys = ON; + + CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL + ); + + CREATE TABLE ssh_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL + ); + + CREATE TABLE host_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT + ); + + CREATE TABLE audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + resource_name TEXT, + details TEXT, + ip_address TEXT, + user_agent TEXT, + success INTEGER NOT NULL, + error_message TEXT, + timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE session_recordings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT NOT NULL, + access_id INTEGER, + started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + ended_at TEXT, + duration INTEGER, + commands TEXT, + dangerous_actions TEXT, + recording_path TEXT, + protocol TEXT NOT NULL DEFAULT 'ssh', + format TEXT NOT NULL DEFAULT 'text', + terminated_by_owner INTEGER DEFAULT 0, + termination_reason TEXT, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL + ); + + INSERT INTO users (id, username) VALUES ('u-1', 'alice'), ('u-2', 'bob'); + INSERT INTO ssh_data (id, name) VALUES (1, 'prod-db'); + + INSERT INTO audit_logs + (user_id, username, action, resource_type, resource_id, success, timestamp) + VALUES + ('u-1', 'alice', 'host.delete', 'host', '1', 1, '2026-07-01 10:00:00'), + ('u-1', 'alice', 'credential.view', 'credential', '9', 1, '2026-07-02 11:00:00'), + ('u-2', 'bob', 'host.create', 'host', '2', 1, '2026-07-03 12:00:00'); + + INSERT INTO session_recordings + (host_id, user_id, started_at, recording_path, protocol, format) + VALUES + (1, 'u-1', '2026-07-01 10:00:00', '/rec/a.guac', 'ssh', 'text'), + (1, 'u-2', '2026-07-03 12:00:00', '/rec/b.guac', 'ssh', 'text'); + `); + return sqlite; +} + +describe("audit retention migration", () => { + it("detects the destructive shape and reports it fixed afterwards", () => { + db = legacyDatabase(); + + expect(userDeleteIsDestructive(db, "audit_logs")).toBe(true); + expect(userDeleteIsDestructive(db, "session_recordings")).toBe(true); + + expect(migrateAuditRetention(db)).toEqual([ + "audit_logs", + "session_recordings", + ]); + + expect(userDeleteIsDestructive(db, "audit_logs")).toBe(false); + expect(userDeleteIsDestructive(db, "session_recordings")).toBe(false); + }); + + it("keeps the audit trail when the user is deleted", () => { + db = legacyDatabase(); + migrateAuditRetention(db); + + db.exec("DELETE FROM users WHERE id = 'u-1'"); + + const rows = db + .prepare( + "SELECT user_id, username, action FROM audit_logs ORDER BY timestamp", + ) + .all() as { user_id: string | null; username: string; action: string }[]; + + expect(rows).toHaveLength(3); + // The account is gone, but the record still names who acted. + expect(rows[0]).toEqual({ + user_id: null, + username: "alice", + action: "host.delete", + }); + expect(rows[2].user_id).toBe("u-2"); + }); + + it("backfills a username onto recordings so they stay attributable", () => { + db = legacyDatabase(); + migrateAuditRetention(db); + + db.exec("DELETE FROM users WHERE id = 'u-1'"); + + const rows = db + .prepare( + "SELECT user_id, username, recording_path FROM session_recordings ORDER BY started_at", + ) + .all() as { user_id: string | null; username: string | null }[]; + + expect(rows).toHaveLength(2); + expect(rows[0].user_id).toBeNull(); + expect(rows[0].username).toBe("alice"); + }); + + it("loses no data in the copy", () => { + db = legacyDatabase(); + const before = db + .prepare("SELECT * FROM audit_logs ORDER BY id") + .all() as Record[]; + + migrateAuditRetention(db); + + const after = db + .prepare("SELECT * FROM audit_logs ORDER BY id") + .all() as Record[]; + + expect(after).toEqual(before); + }); + + it("still cascades recordings when their host is deleted", () => { + db = legacyDatabase(); + migrateAuditRetention(db); + + db.exec("PRAGMA foreign_keys = ON"); + db.exec("DELETE FROM ssh_data WHERE id = 1"); + + expect( + db.prepare("SELECT COUNT(*) AS n FROM session_recordings").get(), + ).toEqual({ n: 0 }); + }); + + it("is idempotent and leaves an already-migrated database alone", () => { + db = legacyDatabase(); + migrateAuditRetention(db); + + const rowsAfterFirst = db.prepare("SELECT * FROM audit_logs").all(); + expect(migrateAuditRetention(db)).toEqual([]); + expect(db.prepare("SELECT * FROM audit_logs").all()).toEqual( + rowsAfterFirst, + ); + }); + + it("does nothing on a database without the tables", () => { + db = new Database(":memory:"); + + expect(() => migrateAuditRetention(db)).not.toThrow(); + expect(migrateAuditRetention(db)).toEqual([]); + }); +}); diff --git a/src/backend/tests/utils/audit-username.test.ts b/src/backend/tests/utils/audit-username.test.ts new file mode 100644 index 0000000..db5e6b1 --- /dev/null +++ b/src/backend/tests/utils/audit-username.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const findById = vi.hoisted(() => vi.fn()); + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentAuditLogRepository: () => ({ create: vi.fn() }), + createCurrentUserRepository: () => ({ findById }), +})); + +const { getAuditUsername, getRequestMeta } = + await import("../../utils/audit-logger.js"); + +beforeEach(() => findById.mockReset()); + +describe("getAuditUsername", () => { + it("resolves the username to store alongside the entry", async () => { + findById.mockResolvedValueOnce({ id: "u-1", username: "alice" }); + + await expect(getAuditUsername("u-1")).resolves.toBe("alice"); + }); + + it("falls back to the id for an account that no longer exists", async () => { + findById.mockResolvedValueOnce(undefined); + + await expect(getAuditUsername("u-gone")).resolves.toBe("u-gone"); + }); + + it("never throws, so it cannot break the operation being audited", async () => { + findById.mockRejectedValueOnce(new Error("database unavailable")); + + await expect(getAuditUsername("u-1")).resolves.toBe("u-1"); + }); +}); + +describe("getRequestMeta", () => { + it("prefers the first x-forwarded-for hop", () => { + const meta = getRequestMeta({ + headers: { + "x-forwarded-for": "203.0.113.9, 10.0.0.1", + "user-agent": "Mozilla/5.0", + }, + ip: "10.0.0.1", + } as never); + + expect(meta).toEqual({ + ipAddress: "203.0.113.9", + userAgent: "Mozilla/5.0", + }); + }); + + it("falls back to the socket address", () => { + const meta = getRequestMeta({ headers: {}, ip: "192.0.2.5" } as never); + + expect(meta.ipAddress).toBe("192.0.2.5"); + expect(meta.userAgent).toBe(""); + }); +}); diff --git a/src/backend/tests/utils/compression-config.test.ts b/src/backend/tests/utils/compression-config.test.ts new file mode 100644 index 0000000..56c0322 --- /dev/null +++ b/src/backend/tests/utils/compression-config.test.ts @@ -0,0 +1,167 @@ +import { afterEach, describe, expect, it } from "vitest"; +import express from "express"; +import type { Server } from "http"; +import { createCompressionMiddleware } from "../../utils/compression-config.js"; + +/** A body big enough to clear the size threshold, and repetitive like real JSON. */ +function bigJson(): Record[] { + return Array.from({ length: 200 }, (_, i) => ({ + id: i, + name: `prod-app-server-${i}`, + ip: `10.20.0.${i % 254}`, + folder: "Production / US-East / App Tier", + enableTerminal: true, + enableTunnel: true, + })); +} + +describe("createCompressionMiddleware", () => { + let server: Server | null = null; + + afterEach(async () => { + if (server) { + await new Promise((resolve) => server!.close(() => resolve())); + server = null; + } + }); + + async function startServer( + configure: (app: express.Express) => void, + ): Promise { + const app = express(); + app.use(createCompressionMiddleware()); + configure(app); + + server = await new Promise((resolve) => { + const s = app.listen(0, "127.0.0.1", () => resolve(s)); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("expected a TCP address"); + } + return `http://127.0.0.1:${address.port}`; + } + + it("gzips a large JSON response", async () => { + const base = await startServer((app) => { + app.get("/big", (_req, res) => res.json(bigJson())); + }); + + const res = await fetch(`${base}/big`, { + headers: { "Accept-Encoding": "gzip" }, + }); + + expect(res.headers.get("content-encoding")).toBe("gzip"); + // fetch transparently decodes, so the parsed body must still be intact. + expect(await res.json()).toHaveLength(200); + }); + + it("substantially shrinks the host-list shaped payload", async () => { + const body = JSON.stringify(bigJson()); + const base = await startServer((app) => { + app.get("/big", (_req, res) => { + res.setHeader("Content-Type", "application/json"); + res.end(body); + }); + }); + + // A gzipped response is sent chunked, so there is no content-length to + // read; measure the encoded bytes off the socket instead. + const res = await fetch(`${base}/big`, { + headers: { "Accept-Encoding": "gzip" }, + }); + expect(res.headers.get("content-encoding")).toBe("gzip"); + + const raw = await new Promise((resolve, reject) => { + import("http").then(({ get }) => { + get( + `${base}/big`, + { headers: { "Accept-Encoding": "gzip" } }, + (response) => { + const chunks: Buffer[] = []; + response.on("data", (c: Buffer) => chunks.push(c)); + response.on("end", () => resolve(Buffer.concat(chunks))); + response.on("error", reject); + }, + ).on("error", reject); + }, reject); + }); + + // Repetitive JSON should compress by well over half. + expect(raw.length).toBeGreaterThan(0); + expect(raw.length).toBeLessThan(Buffer.byteLength(body) / 2); + }); + + it("leaves a small response uncompressed", async () => { + const base = await startServer((app) => { + app.get("/small", (_req, res) => res.json({ ok: true })); + }); + + const res = await fetch(`${base}/small`, { + headers: { "Accept-Encoding": "gzip" }, + }); + + expect(res.headers.get("content-encoding")).toBeNull(); + expect(await res.json()).toEqual({ ok: true }); + }); + + it("does not compress an event stream, which must not be buffered", async () => { + const base = await startServer((app) => { + app.get("/stream", (_req, res) => { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.write(`data: ${"x".repeat(8192)}\n\n`); + res.end(); + }); + }); + + const res = await fetch(`${base}/stream`, { + headers: { "Accept-Encoding": "gzip" }, + }); + await res.text(); + + expect(res.headers.get("content-encoding")).toBeNull(); + }); + + it("does not compress a binary download stream", async () => { + const base = await startServer((app) => { + app.get("/download", (_req, res) => { + res.setHeader("Content-Type", "application/octet-stream"); + res.end(Buffer.alloc(16384, 1)); + }); + }); + + const res = await fetch(`${base}/download`, { + headers: { "Accept-Encoding": "gzip" }, + }); + await res.arrayBuffer(); + + expect(res.headers.get("content-encoding")).toBeNull(); + }); + + it("honours an explicit opt-out header", async () => { + const base = await startServer((app) => { + app.get("/big", (_req, res) => res.json(bigJson())); + }); + + const res = await fetch(`${base}/big`, { + headers: { "Accept-Encoding": "gzip", "x-no-compression": "1" }, + }); + await res.json(); + + expect(res.headers.get("content-encoding")).toBeNull(); + }); + + it("leaves the body alone for a client that cannot accept gzip", async () => { + const base = await startServer((app) => { + app.get("/big", (_req, res) => res.json(bigJson())); + }); + + const res = await fetch(`${base}/big`, { + headers: { "Accept-Encoding": "identity" }, + }); + + expect(res.headers.get("content-encoding")).toBeNull(); + expect(await res.json()).toHaveLength(200); + }); +}); diff --git a/src/backend/tests/utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.test.ts b/src/backend/tests/utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.test.ts new file mode 100644 index 0000000..ecb8431 --- /dev/null +++ b/src/backend/tests/utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.test.ts @@ -0,0 +1,193 @@ +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + sqlite: null as unknown as Database.Database, + settings: new Map(), + resyncedHostIds: [] as number[], + saves: [] as string[], +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentSettingsRepository: () => ({ + get: async (key: string) => state.settings.get(key) ?? null, + set: async (key: string, value: string) => { + state.settings.set(key, value); + }, + }), + getCurrentRepositorySqlite: () => state.sqlite, +})); + +vi.mock("../../../utils/shared-host-secrets-manager.js", () => ({ + SharedHostSecretsManager: { + getInstance: () => ({ + resyncHost: async (hostId: number) => { + state.resyncedHostIds.push(hostId); + }, + }), + }, +})); + +vi.mock("../../../utils/database-save-trigger.js", () => ({ + DatabaseSaveTrigger: { + forceSave: async (reason: string) => { + state.saves.push(reason); + }, + }, +})); + +vi.mock("../../../utils/logger.js", () => ({ + databaseLogger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + success: vi.fn(), + }, +})); + +import { runLegacySharedSshAuthOptInMigration } from "../../../utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.js"; + +beforeEach(() => { + state.sqlite = new Database(":memory:"); + state.sqlite.exec(` + CREATE TABLE ssh_data ( + id INTEGER PRIMARY KEY, + share_ssh_auth INTEGER NOT NULL DEFAULT 0 + ); + CREATE TABLE host_access ( + id INTEGER PRIMARY KEY, + host_id INTEGER NOT NULL, + expires_at TEXT + ); + CREATE TABLE shared_host_secrets ( + id INTEGER PRIMARY KEY, + host_access_id INTEGER NOT NULL, + protocol TEXT NOT NULL + ); + INSERT INTO ssh_data (id, share_ssh_auth) + VALUES (1, 0), (2, 0), (3, 1), (4, 0), (5, 0); + INSERT INTO host_access (id, host_id, expires_at) + VALUES + (10, 1, NULL), + (30, 3, NULL), + (40, 4, NULL), + (50, 5, '2000-01-01T00:00:00.000Z'); + INSERT INTO shared_host_secrets (id, host_access_id, protocol) + VALUES + (100, 10, 'ssh'), + (400, 40, 'rdp'), + (500, 50, 'ssh'); + `); + state.settings.clear(); + state.resyncedHostIds = []; + state.saves = []; +}); + +afterEach(() => { + state.sqlite.close(); + delete process.env.DATABASE_DIALECT; +}); + +describe("runLegacySharedSshAuthOptInMigration", () => { + it("preserves preexisting sharing while leaving unshared hosts private", async () => { + await expect(runLegacySharedSshAuthOptInMigration()).resolves.toEqual({ + enabled: 1, + resynced: 2, + skipped: 0, + }); + + expect( + state.sqlite + .prepare("SELECT id, share_ssh_auth FROM ssh_data ORDER BY id") + .all(), + ).toEqual([ + { id: 1, share_ssh_auth: 1 }, + { id: 2, share_ssh_auth: 0 }, + { id: 3, share_ssh_auth: 1 }, + { id: 4, share_ssh_auth: 0 }, + { id: 5, share_ssh_auth: 0 }, + ]); + expect(state.resyncedHostIds).toEqual([1, 3]); + expect(state.settings.get("legacy_shared_ssh_auth_opt_in_v1")).toBe("done"); + expect(state.saves).toEqual(["legacy_shared_ssh_auth_opt_in_migration"]); + }); + + it("recognizes a legacy SSH credential snapshot as prior sharing evidence", async () => { + state.sqlite.exec(` + CREATE TABLE shared_credentials ( + id INTEGER PRIMARY KEY, + host_access_id INTEGER NOT NULL + ); + INSERT INTO shared_credentials (id, host_access_id) VALUES (1, 40); + `); + + await expect(runLegacySharedSshAuthOptInMigration()).resolves.toEqual({ + enabled: 2, + resynced: 3, + skipped: 0, + }); + + expect( + state.sqlite + .prepare("SELECT share_ssh_auth FROM ssh_data WHERE id = 4") + .get(), + ).toEqual({ share_ssh_auth: 1 }); + expect(state.resyncedHostIds).toEqual([1, 3, 4]); + }); + + it("is idempotent", async () => { + await runLegacySharedSshAuthOptInMigration(); + state.resyncedHostIds = []; + state.saves = []; + + expect(await runLegacySharedSshAuthOptInMigration()).toBeNull(); + expect(state.resyncedHostIds).toEqual([]); + expect(state.saves).toEqual([]); + }); + + it("does not re-share private hosts after the privacy migration has run", async () => { + state.settings.set("private_shared_ssh_auth_v1", "done"); + + await expect(runLegacySharedSshAuthOptInMigration()).resolves.toEqual({ + enabled: 0, + resynced: 1, + skipped: 0, + }); + + expect( + state.sqlite + .prepare("SELECT share_ssh_auth FROM ssh_data WHERE id = 1") + .get(), + ).toEqual({ share_ssh_auth: 0 }); + expect(state.resyncedHostIds).toEqual([3]); + }); + + // The behavior being preserved belongs to releases that only ran on SQLite, + // and the probes below it are sqlite_master specific. Without the guard this + // logged a failure on every boot against a remote engine. + it.each(["postgres", "mysql"])("does nothing on %s", async (dialect) => { + process.env.DATABASE_DIALECT = dialect; + + await expect(runLegacySharedSshAuthOptInMigration()).resolves.toEqual({ + enabled: 0, + resynced: 0, + skipped: 0, + }); + + expect( + state.sqlite + .prepare("SELECT id, share_ssh_auth FROM ssh_data ORDER BY id") + .all(), + ).toEqual([ + { id: 1, share_ssh_auth: 0 }, + { id: 2, share_ssh_auth: 0 }, + { id: 3, share_ssh_auth: 1 }, + { id: 4, share_ssh_auth: 0 }, + { id: 5, share_ssh_auth: 0 }, + ]); + expect(state.resyncedHostIds).toEqual([]); + expect(state.settings.has("legacy_shared_ssh_auth_opt_in_v1")).toBe(false); + expect(state.saves).toEqual([]); + }); +}); diff --git a/src/backend/tests/utils/crypto-migration/private-shared-ssh-auth-migration.test.ts b/src/backend/tests/utils/crypto-migration/private-shared-ssh-auth-migration.test.ts new file mode 100644 index 0000000..79ff7cf --- /dev/null +++ b/src/backend/tests/utils/crypto-migration/private-shared-ssh-auth-migration.test.ts @@ -0,0 +1,121 @@ +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + sqlite: null as unknown as Database.Database, + settings: new Map(), + saves: [] as string[], +})); + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentSettingsRepository: () => ({ + get: async (key: string) => state.settings.get(key) ?? null, + set: async (key: string, value: string) => { + state.settings.set(key, value); + }, + }), + getCurrentRepositorySqlite: () => state.sqlite, +})); + +vi.mock("../../../utils/database-save-trigger.js", () => ({ + DatabaseSaveTrigger: { + forceSave: async (reason: string) => { + state.saves.push(reason); + }, + }, +})); + +vi.mock("../../../utils/logger.js", () => ({ + databaseLogger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + success: vi.fn(), + }, +})); + +import { runPrivateSharedSshAuthMigration } from "../../../utils/crypto-migration/private-shared-ssh-auth-migration.js"; + +beforeEach(() => { + state.sqlite = new Database(":memory:"); + state.sqlite.exec(` + CREATE TABLE ssh_data ( + id INTEGER PRIMARY KEY, + share_ssh_auth INTEGER NOT NULL DEFAULT 0 + ); + CREATE TABLE host_access ( + id INTEGER PRIMARY KEY, + host_id INTEGER NOT NULL + ); + CREATE TABLE shared_host_secrets ( + id INTEGER PRIMARY KEY, + host_access_id INTEGER NOT NULL, + protocol TEXT NOT NULL + ); + INSERT INTO ssh_data (id, share_ssh_auth) + VALUES (1, 1), (2, 0); + INSERT INTO host_access (id, host_id) + VALUES (10, 1), (20, 2); + INSERT INTO shared_host_secrets (id, host_access_id, protocol) + VALUES + (1, 10, 'ssh'), + (2, 10, 'rdp'), + (3, 20, 'ssh'), + (4, 20, 'vnc'); + `); + state.settings.clear(); + state.saves = []; +}); + +afterEach(() => { + state.sqlite.close(); + delete process.env.DATABASE_DIALECT; +}); + +describe("runPrivateSharedSshAuthMigration", () => { + it("preserves opted-in SSH snapshots and removes only private ones", async () => { + expect(await runPrivateSharedSshAuthMigration()).toBe(1); + expect( + state.sqlite + .prepare( + "SELECT host_access_id, protocol FROM shared_host_secrets ORDER BY id", + ) + .all(), + ).toEqual([ + { host_access_id: 10, protocol: "ssh" }, + { host_access_id: 10, protocol: "rdp" }, + { host_access_id: 20, protocol: "vnc" }, + ]); + expect(state.settings.get("private_shared_ssh_auth_v1")).toBe("done"); + expect(state.saves).toEqual(["private_shared_ssh_auth_migration"]); + }); + + it("is idempotent", async () => { + state.settings.set("private_shared_ssh_auth_v1", "done"); + + expect(await runPrivateSharedSshAuthMigration()).toBeNull(); + expect( + state.sqlite + .prepare("SELECT COUNT(*) AS count FROM shared_host_secrets") + .get(), + ).toEqual({ count: 4 }); + expect(state.saves).toHaveLength(0); + }); + + // These snapshots only exist in databases written before Postgres and MySQL + // were supported. Without the guard this reached for a SQLite handle that is + // not there and logged a failure on every boot. + it.each(["postgres", "mysql"])("does nothing on %s", async (dialect) => { + process.env.DATABASE_DIALECT = dialect; + + expect(await runPrivateSharedSshAuthMigration()).toBeNull(); + expect( + state.sqlite + .prepare("SELECT COUNT(*) AS count FROM shared_host_secrets") + .get(), + ).toEqual({ count: 4 }); + expect(state.settings.has("private_shared_ssh_auth_v1")).toBe(false); + expect(state.saves).toHaveLength(0); + }); +}); diff --git a/src/backend/tests/utils/data-dir-guard.test.ts b/src/backend/tests/utils/data-dir-guard.test.ts new file mode 100644 index 0000000..c1aa095 --- /dev/null +++ b/src/backend/tests/utils/data-dir-guard.test.ts @@ -0,0 +1,135 @@ +import { afterEach, describe, expect, it } from "vitest"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { + ALLOW_EMPTY_DATA_DIR_ENV, + assertDataDirIsNotMisconfigured, + DataDirMisconfiguredError, + findDatabaseOutsideDataDir, +} from "../../utils/data-dir-guard.js"; + +const tempDirs: string[] = []; + +function makeTempDir(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-datadir-")); + tempDirs.push(dir); + return dir; +} + +/** Writes a plain (unencrypted) database file into `dir`. */ +function writePlainDatabase(dir: string, size = 4096): string { + fs.mkdirSync(dir, { recursive: true }); + const dbPath = path.join(dir, "db.sqlite"); + fs.writeFileSync(dbPath, Buffer.alloc(size, 1)); + return dbPath; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("findDatabaseOutsideDataDir", () => { + it("returns null on a genuinely fresh install", () => { + const cwd = makeTempDir(); + const dataDir = path.join(cwd, "db", "data"); + + expect(findDatabaseOutsideDataDir(dataDir, cwd)).toBeNull(); + }); + + it("finds a database left in the legacy data directory", () => { + const cwd = makeTempDir(); + const legacyDir = path.join(cwd, "data"); + writePlainDatabase(legacyDir); + + expect(findDatabaseOutsideDataDir(path.join(cwd, "db", "data"), cwd)).toBe( + legacyDir, + ); + }); + + it("finds a database under the default directory when DATA_DIR points elsewhere", () => { + const cwd = makeTempDir(); + const defaultDir = path.join(cwd, "db", "data"); + writePlainDatabase(defaultDir); + + expect(findDatabaseOutsideDataDir("/mnt/unmounted-volume", cwd)).toBe( + defaultDir, + ); + }); + + it("ignores the configured data directory itself", () => { + const cwd = makeTempDir(); + const dataDir = path.join(cwd, "data"); + writePlainDatabase(dataDir); + + expect(findDatabaseOutsideDataDir(dataDir, cwd)).toBeNull(); + }); + + it("ignores a zero-length database file", () => { + const cwd = makeTempDir(); + writePlainDatabase(path.join(cwd, "data"), 0); + + expect(findDatabaseOutsideDataDir(path.join(cwd, "db", "data"), cwd)).toBe( + null, + ); + }); +}); + +describe("assertDataDirIsNotMisconfigured", () => { + it("passes when no database exists anywhere else", () => { + const cwd = makeTempDir(); + + expect(() => + assertDataDirIsNotMisconfigured(path.join(cwd, "db", "data"), {}, cwd), + ).not.toThrow(); + }); + + it("refuses to start and names both directories", () => { + const cwd = makeTempDir(); + const legacyDir = path.join(cwd, "data"); + writePlainDatabase(legacyDir); + const dataDir = path.join(cwd, "db", "data"); + + expect(() => assertDataDirIsNotMisconfigured(dataDir, {}, cwd)).toThrow( + DataDirMisconfiguredError, + ); + // Matched as substrings, not patterns: Windows paths are full of + // backslash sequences a RegExp would read as escapes. + expect(() => assertDataDirIsNotMisconfigured(dataDir, {}, cwd)).toThrow( + legacyDir, + ); + expect(() => assertDataDirIsNotMisconfigured(dataDir, {}, cwd)).toThrow( + dataDir, + ); + }); + + it("can be overridden to start with a new database", () => { + const cwd = makeTempDir(); + writePlainDatabase(path.join(cwd, "data")); + + for (const value of ["true", "1", "YES", "on"]) { + expect(() => + assertDataDirIsNotMisconfigured( + path.join(cwd, "db", "data"), + { [ALLOW_EMPTY_DATA_DIR_ENV]: value }, + cwd, + ), + ).not.toThrow(); + } + }); + + it("still refuses when the override is not a truthy value", () => { + const cwd = makeTempDir(); + writePlainDatabase(path.join(cwd, "data")); + + expect(() => + assertDataDirIsNotMisconfigured( + path.join(cwd, "db", "data"), + { [ALLOW_EMPTY_DATA_DIR_ENV]: "false" }, + cwd, + ), + ).toThrow(DataDirMisconfiguredError); + }); +}); diff --git a/src/backend/tests/utils/database-save-trigger.test.ts b/src/backend/tests/utils/database-save-trigger.test.ts index 82b38e1..c33d33a 100644 --- a/src/backend/tests/utils/database-save-trigger.test.ts +++ b/src/backend/tests/utils/database-save-trigger.test.ts @@ -38,4 +38,28 @@ describe("DatabaseSaveTrigger", () => { expect(DatabaseSaveTrigger.isDirty).toBe(false); expect(DatabaseSaveTrigger.getStatus().pendingSave).toBe(false); }); + + it("queues a force save behind an in-flight save", async () => { + let finishFirstSave: (() => void) | undefined; + const firstSave = new Promise((resolve) => { + finishFirstSave = resolve; + }); + const save = vi + .fn<() => Promise>() + .mockReturnValueOnce(firstSave) + .mockResolvedValueOnce(undefined); + DatabaseSaveTrigger.initialize(save); + + const first = DatabaseSaveTrigger.forceSave("first_write"); + await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1)); + + const second = DatabaseSaveTrigger.forceSave("sso_provider_write"); + expect(save).toHaveBeenCalledTimes(1); + + finishFirstSave?.(); + await Promise.all([first, second]); + + expect(save).toHaveBeenCalledTimes(2); + expect(DatabaseSaveTrigger.getStatus().pendingSave).toBe(false); + }); }); diff --git a/src/backend/tests/utils/permission-manager.test.ts b/src/backend/tests/utils/permission-manager.test.ts index ae705d0..ccd98c5 100644 --- a/src/backend/tests/utils/permission-manager.test.ts +++ b/src/backend/tests/utils/permission-manager.test.ts @@ -24,6 +24,11 @@ const accessState = vi.hoisted(() => ({ } | null, touched: [] as number[], adminIds: new Set(), + rolePermissionCalls: 0, + rolePermissions: [] as { permissions: string }[], + ownedHostIds: new Set(), + visibleGrants: [] as { hostId: number }[], + ownedQueryCalls: 0, })); vi.mock("../../database/repositories/factory.js", () => ({ @@ -31,8 +36,13 @@ vi.mock("../../database/repositories/factory.js", () => ({ isHostOwnedByUser: async (_hostId: number, userId: string) => userId === accessState.ownerId, findHostOwnerId: async () => accessState.ownerId, + listOwnedHostIds: async () => { + accessState.ownedQueryCalls += 1; + return accessState.ownedHostIds; + }, }), createCurrentRbacAccessRepository: () => ({ + listVisibleHostAccessEntries: async () => accessState.visibleGrants, findActiveHostAccess: async () => accessState.grant, touchHostAccess: async (id: number) => { accessState.touched.push(id); @@ -41,7 +51,10 @@ vi.mock("../../database/repositories/factory.js", () => ({ }), createCurrentRoleRepository: () => ({ listUserRoleIds: async () => [], - listUserRolePermissions: async () => [], + listUserRolePermissions: async () => { + accessState.rolePermissionCalls += 1; + return accessState.rolePermissions; + }, userHasAnyRoleName: async () => false, }), createCurrentUserRepository: () => ({ @@ -195,3 +208,160 @@ describe("PermissionManager.canAccessHost level hierarchy", () => { expect(info.isAdminBypass).toBeUndefined(); }); }); + +describe("PermissionManager.getUserPermissions caching", () => { + let manager: PermissionManagerInstance; + + beforeEach(() => { + vi.restoreAllMocks(); + manager = PermissionManager.getInstance(); + accessState.rolePermissionCalls = 0; + accessState.rolePermissions = [{ permissions: '["hosts.read"]' }]; + manager.invalidateUserPermissionCache("cache-user"); + }); + + it("serves repeat lookups from cache instead of re-querying roles", async () => { + expect(await manager.getUserPermissions("cache-user")).toEqual([ + "hosts.read", + ]); + expect(await manager.getUserPermissions("cache-user")).toEqual([ + "hosts.read", + ]); + + expect(accessState.rolePermissionCalls).toBe(1); + }); + + it("re-reads roles after an explicit invalidation", async () => { + await manager.getUserPermissions("cache-user"); + manager.invalidateUserPermissionCache("cache-user"); + accessState.rolePermissions = [{ permissions: '["hosts.write"]' }]; + + expect(await manager.getUserPermissions("cache-user")).toEqual([ + "hosts.write", + ]); + expect(accessState.rolePermissionCalls).toBe(2); + }); + + it("expires an entry once its own TTL has passed", async () => { + vi.useFakeTimers(); + try { + await manager.getUserPermissions("cache-user"); + // Just past the 5 minute TTL. + vi.advanceTimersByTime(5 * 60 * 1000 + 1); + await manager.getUserPermissions("cache-user"); + + expect(accessState.rolePermissionCalls).toBe(2); + } finally { + vi.useRealTimers(); + } + }); + + it("keeps a still-fresh entry when the sweep runs", async () => { + vi.useFakeTimers(); + try { + await manager.getUserPermissions("cache-user"); + // Fire the periodic sweep without crossing this entry's own TTL. The + // old implementation cleared the whole map here, expiring every active + // user at once. + vi.advanceTimersByTime(5 * 60 * 1000 - 1000); + await manager.getUserPermissions("cache-user"); + + expect(accessState.rolePermissionCalls).toBe(1); + } finally { + vi.useRealTimers(); + } + }); + + it("returns an empty set rather than throwing when role lookup fails", async () => { + manager.invalidateUserPermissionCache("boom-user"); + accessState.rolePermissions = [{ permissions: "not-json" }]; + + expect(await manager.getUserPermissions("boom-user")).toEqual([]); + }); +}); + +describe("PermissionManager.filterAccessibleHostIds", () => { + let manager: PermissionManagerInstance; + + beforeEach(() => { + vi.restoreAllMocks(); + manager = PermissionManager.getInstance(); + accessState.adminIds = new Set(); + accessState.ownedHostIds = new Set(); + accessState.visibleGrants = []; + accessState.ownedQueryCalls = 0; + }); + + it("keeps hosts the user owns", async () => { + accessState.ownedHostIds = new Set([1, 2]); + + const allowed = await manager.filterAccessibleHostIds("u1", [1, 2, 3]); + + expect([...allowed].sort()).toEqual([1, 2]); + }); + + it("keeps hosts shared with the user", async () => { + accessState.visibleGrants = [{ hostId: 7 }]; + + const allowed = await manager.filterAccessibleHostIds("u1", [7, 8]); + + expect([...allowed]).toEqual([7]); + }); + + it("combines owned and shared without duplicating", async () => { + accessState.ownedHostIds = new Set([1]); + accessState.visibleGrants = [{ hostId: 1 }, { hostId: 2 }]; + + const allowed = await manager.filterAccessibleHostIds("u1", [1, 2, 3]); + + expect([...allowed].sort()).toEqual([1, 2]); + }); + + it("excludes another tenant's hosts", async () => { + accessState.ownedHostIds = new Set([1]); + + const allowed = await manager.filterAccessibleHostIds("u1", [1, 99, 100]); + + expect(allowed.has(99)).toBe(false); + expect(allowed.has(100)).toBe(false); + }); + + it("gives an admin every host without per-host lookups", async () => { + accessState.adminIds = new Set(["admin1"]); + + const allowed = await manager.filterAccessibleHostIds( + "admin1", + [1, 2, 3, 4], + ); + + expect([...allowed].sort()).toEqual([1, 2, 3, 4]); + }); + + it("resolves the whole fleet with a single owned-hosts query", async () => { + accessState.ownedHostIds = new Set( + Array.from({ length: 500 }, (_, i) => i + 1), + ); + const ids = Array.from({ length: 500 }, (_, i) => i + 1); + + const allowed = await manager.filterAccessibleHostIds("u1", ids); + + expect(allowed.size).toBe(500); + // The point of the batch path: cost does not scale with host count. + expect(accessState.ownedQueryCalls).toBe(1); + }); + + it("short-circuits an empty list without querying", async () => { + const allowed = await manager.filterAccessibleHostIds("u1", []); + + expect(allowed.size).toBe(0); + expect(accessState.ownedQueryCalls).toBe(0); + }); + + it("fails closed when the lookup throws", async () => { + accessState.ownedHostIds = null as unknown as Set; + + const allowed = await manager.filterAccessibleHostIds("u1", [1, 2]); + + expect(allowed.size).toBe(0); + }); +}); diff --git a/src/backend/tests/utils/request-origin.test.ts b/src/backend/tests/utils/request-origin.test.ts index 39d712c..55c3639 100644 --- a/src/backend/tests/utils/request-origin.test.ts +++ b/src/backend/tests/utils/request-origin.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; import { + getClientIp, getRequestBasePath, getRequestBaseUrl, getRequestBaseUrlWithForceHTTPS, @@ -14,6 +15,18 @@ function request(headers: Record) { } as Parameters[0]; } +function requestWithSocket( + headers: Record, + socket: { remoteAddress?: string }, + ip?: string, +) { + return { + headers, + socket, + ip, + } as unknown as Parameters[0]; +} + function restoreEnv(name: string, value: string | undefined) { if (value === undefined) { delete process.env[name]; @@ -108,6 +121,52 @@ describe("getRequestBasePath", () => { }); }); +describe("getClientIp", () => { + it("prefers the leftmost X-Forwarded-For entry over the socket peer", () => { + expect( + getClientIp( + requestWithSocket( + { "x-forwarded-for": "203.0.113.7, 10.0.0.1, 10.0.0.2" }, + { remoteAddress: "::ffff:127.0.0.1" }, + ), + ), + ).toBe("203.0.113.7"); + }); + + it("handles X-Forwarded-For sent as a header array", () => { + expect( + getClientIp( + requestWithSocket( + { "x-forwarded-for": ["203.0.113.7", "10.0.0.1"] }, + { remoteAddress: "::ffff:127.0.0.1" }, + ), + ), + ).toBe("203.0.113.7"); + }); + + it("falls back to req.ip when there is no forwarded header", () => { + expect( + getClientIp( + requestWithSocket( + {}, + { remoteAddress: "::ffff:127.0.0.1" }, + "198.51.100.5", + ), + ), + ).toBe("198.51.100.5"); + }); + + it("falls back to the raw socket peer when nothing else is available", () => { + expect( + getClientIp(requestWithSocket({}, { remoteAddress: "198.51.100.9" })), + ).toBe("198.51.100.9"); + }); + + it("returns unknown when no IP information exists at all", () => { + expect(getClientIp(requestWithSocket({}, {}))).toBe("unknown"); + }); +}); + describe("getRequestOrigin", () => { it("ignores non-numeric forwarded ports", () => { expect( diff --git a/src/backend/tests/utils/safe-outbound-fetch.test.ts b/src/backend/tests/utils/safe-outbound-fetch.test.ts new file mode 100644 index 0000000..e2fc1b2 --- /dev/null +++ b/src/backend/tests/utils/safe-outbound-fetch.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, it, vi } from "vitest"; +import type { LookupAddress, LookupOptions } from "dns"; +import { + createDnsLookupHook, + isBlockedAddress, +} from "../../utils/safe-outbound-fetch.js"; + +describe("isBlockedAddress", () => { + it("allows public IPv4 addresses", () => { + expect(isBlockedAddress("8.8.8.8")).toBe(false); + expect(isBlockedAddress("104.21.52.150")).toBe(false); + }); + + it("blocks private/reserved IPv4 ranges", () => { + expect(isBlockedAddress("10.0.0.1")).toBe(true); + expect(isBlockedAddress("172.16.0.1")).toBe(true); + expect(isBlockedAddress("192.168.1.1")).toBe(true); + expect(isBlockedAddress("127.0.0.1")).toBe(true); + expect(isBlockedAddress("169.254.1.1")).toBe(true); + expect(isBlockedAddress("100.64.0.1")).toBe(true); + }); + + it("allows public IPv6 addresses", () => { + expect(isBlockedAddress("2606:4700:3034::ac43:c88d")).toBe(false); + expect(isBlockedAddress("2001:4860:4860::8888")).toBe(false); + }); + + it("blocks private/reserved IPv6 ranges", () => { + expect(isBlockedAddress("::1")).toBe(true); + expect(isBlockedAddress("fc00::1")).toBe(true); + expect(isBlockedAddress("fe80::1")).toBe(true); + }); + + it("blocks IPv4-mapped-IPv6 spoofing of private addresses", () => { + expect(isBlockedAddress("::ffff:127.0.0.1")).toBe(true); + expect(isBlockedAddress("::ffff:192.168.1.1")).toBe(true); + expect(isBlockedAddress("::ffff:10.0.0.1")).toBe(true); + }); + + it("does not block IPv4-mapped-IPv6 form of public addresses", () => { + expect(isBlockedAddress("::ffff:104.21.52.150")).toBe(false); + expect(isBlockedAddress("::ffff:8.8.8.8")).toBe(false); + }); + + it("blocks unparseable input", () => { + expect(isBlockedAddress("not-an-ip")).toBe(true); + }); +}); + +function runHook( + addresses: LookupAddress[] | string | undefined, + error: NodeJS.ErrnoException | null = null, + lookupOptions: LookupOptions = { all: true }, +) { + const fakeLookup = vi.fn( + ( + _host: string, + _opts: LookupOptions, + cb: ( + err: NodeJS.ErrnoException | null, + addrs: LookupAddress[] | string | undefined, + family?: number, + ) => void, + ) => { + cb(error, addresses, typeof addresses === "string" ? 4 : undefined); + }, + ); + + const hook = createDnsLookupHook(fakeLookup); + const callback = vi.fn(); + + hook("example.invalid", lookupOptions, callback); + + return { callback, fakeLookup }; +} + +const lookupOptionsCases: Array<[string, LookupOptions, unknown[]]> = [ + ["all:true", { all: true }, ["", 0]], + ["all:false", { all: false }, ["", 0]], + ["all omitted", {}, ["", 0]], +]; + +const publicAddresses: LookupAddress[] = [ + { + address: "104.21.52.150", + family: 4, + }, + { + address: "2606:4700:3034::ac43:c88d", + family: 6, + }, +]; + +describe("createDnsLookupHook", () => { + it("allows a public IPv4 address through", () => { + const { callback } = runHook([ + { + address: "104.21.52.150", + family: 4, + }, + ]); + + expect(callback).toHaveBeenCalledWith( + null, + [{ address: "104.21.52.150", family: 4 }], + 0, + ); + }); + + it.each(lookupOptionsCases)( + "rejects if any address is private, including an IPv4-mapped IPv6 spoof not in first position (%s)", + (_label, lookupOptions, tailArgs) => { + const { callback } = runHook( + [ + { + address: "104.21.52.150", + family: 4, + }, + { + address: "::ffff:192.168.1.1", + family: 6, + }, + { + address: "2606:4700:3034::ac43:c88d", + family: 6, + }, + ], + null, + lookupOptions, + ); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Private destinations are not allowed", + }), + ...tailArgs, + ); + }, + ); + + it("returns a single lookup result when all is false", () => { + const { callback } = runHook("104.21.52.150", null, { all: false }); + + expect(callback).toHaveBeenCalledWith(null, "104.21.52.150", 4); + }); + + it("returns a single lookup result when all is omitted", () => { + const { callback } = runHook("104.21.52.150", null, {}); + + expect(callback).toHaveBeenCalledWith(null, "104.21.52.150", 4); + }); + + it("returns all lookup results when all is true", () => { + const { callback } = runHook(publicAddresses, null, { all: true }); + + expect(callback).toHaveBeenCalledWith(null, publicAddresses, 0); + }); + + it("rejects invalid single lookup results with a DNS lookup error", () => { + const { callback } = runHook(undefined, null, { all: false }); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "DNS lookup returned invalid address", + }), + "", + 0, + ); + }); + + it("rejects with a distinct error when DNS returns no addresses", () => { + const { callback } = runHook([]); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "DNS resolution returned no addresses", + }), + "", + 0, + ); + }); + + it("propagates a real DNS lookup error untouched", () => { + const dnsError = Object.assign(new Error("getaddrinfo ENOTFOUND"), { + code: "ENOTFOUND", + }); + + const { callback } = runHook([], dnsError); + + expect(callback).toHaveBeenCalledWith(dnsError, "", 0); + }); + + it("always asks the underlying resolver for all:true regardless of the caller's option", () => { + const { fakeLookup } = runHook( + [{ address: "104.21.52.150", family: 4 }], + null, + { all: false }, + ); + + console.log(fakeLookup.mock.calls); + + expect(fakeLookup).toHaveBeenCalledWith( + "example.invalid", + expect.objectContaining({ + all: true, + verbatim: true, + }), + expect.any(Function), + ); + }); +}); diff --git a/src/backend/tests/utils/shared-host-auth-override-migration.test.ts b/src/backend/tests/utils/shared-host-auth-override-migration.test.ts new file mode 100644 index 0000000..92dc0f5 --- /dev/null +++ b/src/backend/tests/utils/shared-host-auth-override-migration.test.ts @@ -0,0 +1,166 @@ +import Database from "better-sqlite3"; +import { afterEach, describe, expect, it } from "vitest"; +import { + ensureSharedHostAuthOverrideProtocolSchema, + migrateLegacySharedHostAuthOverrides, +} from "../../utils/shared-host-auth-override-migration.js"; + +describe("migrateLegacySharedHostAuthOverrides", () => { + let sqlite: Database.Database | null = null; + + afterEach(() => { + sqlite?.close(); + sqlite = null; + }); + + it("creates protocol-aware storage with SSH as the default", () => { + sqlite = new Database(":memory:"); + sqlite.exec(` + CREATE TABLE users (id TEXT PRIMARY KEY); + CREATE TABLE ssh_data (id INTEGER PRIMARY KEY); + CREATE TABLE ssh_credentials (id INTEGER PRIMARY KEY); + INSERT INTO users (id) VALUES ('recipient'); + INSERT INTO ssh_data (id) VALUES (42); + INSERT INTO ssh_credentials (id) VALUES (7); + `); + + expect(ensureSharedHostAuthOverrideProtocolSchema(sqlite)).toBe("created"); + sqlite + .prepare( + "INSERT INTO shared_host_auth_overrides (host_id, user_id, credential_id) VALUES (?, ?, ?)", + ) + .run(42, "recipient", 7); + expect( + sqlite + .prepare( + "SELECT protocol, credential_id FROM shared_host_auth_overrides", + ) + .get(), + ).toEqual({ protocol: "ssh", credential_id: 7 }); + expect(ensureSharedHostAuthOverrideProtocolSchema(sqlite)).toBe("current"); + }); + + it("moves direct-share overrides once and clears the legacy column", () => { + sqlite = new Database(":memory:"); + sqlite.exec(` + CREATE TABLE host_access ( + id INTEGER PRIMARY KEY, + host_id INTEGER NOT NULL, + user_id TEXT, + role_id INTEGER, + override_credential_id INTEGER + ); + CREATE TABLE shared_host_auth_overrides ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT NOT NULL, + protocol TEXT NOT NULL DEFAULT 'ssh', + credential_id INTEGER NOT NULL, + UNIQUE(host_id, user_id, protocol) + ); + INSERT INTO host_access + (id, host_id, user_id, role_id, override_credential_id) + VALUES + (1, 42, 'direct-user', NULL, 7), + (2, 42, NULL, 3, 8), + (3, 43, 'no-override', NULL, NULL); + `); + const settings = new Map(); + + expect( + migrateLegacySharedHostAuthOverrides( + sqlite, + (key) => settings.get(key) ?? null, + (key, value) => settings.set(key, value), + ), + ).toBe(true); + + expect( + sqlite + .prepare( + "SELECT host_id, user_id, protocol, credential_id FROM shared_host_auth_overrides", + ) + .all(), + ).toEqual([ + { + host_id: 42, + user_id: "direct-user", + protocol: "ssh", + credential_id: 7, + }, + ]); + expect( + sqlite + .prepare("SELECT override_credential_id FROM host_access WHERE id = 1") + .get(), + ).toEqual({ override_credential_id: null }); + + sqlite + .prepare("UPDATE host_access SET override_credential_id = 9 WHERE id = 1") + .run(); + expect( + migrateLegacySharedHostAuthOverrides( + sqlite, + (key) => settings.get(key) ?? null, + (key, value) => settings.set(key, value), + ), + ).toBe(false); + expect( + sqlite + .prepare( + "SELECT credential_id FROM shared_host_auth_overrides WHERE host_id = 42", + ) + .get(), + ).toEqual({ credential_id: 7 }); + }); + + it("preserves pre-protocol rows as SSH and permits protocol isolation", () => { + sqlite = new Database(":memory:"); + sqlite.exec(` + CREATE TABLE users (id TEXT PRIMARY KEY); + CREATE TABLE ssh_data (id INTEGER PRIMARY KEY); + CREATE TABLE ssh_credentials (id INTEGER PRIMARY KEY); + CREATE TABLE shared_host_auth_overrides ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT NOT NULL, + credential_id INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(host_id, user_id) + ); + INSERT INTO users (id) VALUES ('recipient'); + INSERT INTO ssh_data (id) VALUES (42); + INSERT INTO ssh_credentials (id) VALUES (7), (8); + INSERT INTO shared_host_auth_overrides + (host_id, user_id, credential_id) + VALUES (42, 'recipient', 7); + `); + + expect(ensureSharedHostAuthOverrideProtocolSchema(sqlite)).toBe("migrated"); + expect( + sqlite + .prepare( + "SELECT protocol, credential_id FROM shared_host_auth_overrides WHERE host_id = 42", + ) + .all(), + ).toEqual([{ protocol: "ssh", credential_id: 7 }]); + + sqlite + .prepare( + "INSERT INTO shared_host_auth_overrides (host_id, user_id, protocol, credential_id) VALUES (?, ?, ?, ?)", + ) + .run(42, "recipient", "rdp", 8); + expect( + sqlite + .prepare( + "SELECT protocol, credential_id FROM shared_host_auth_overrides ORDER BY protocol", + ) + .all(), + ).toEqual([ + { protocol: "rdp", credential_id: 8 }, + { protocol: "ssh", credential_id: 7 }, + ]); + expect(ensureSharedHostAuthOverrideProtocolSchema(sqlite)).toBe("current"); + }); +}); diff --git a/src/backend/tests/utils/shared-host-secrets-manager.test.ts b/src/backend/tests/utils/shared-host-secrets-manager.test.ts index 9c41e80..4785db7 100644 --- a/src/backend/tests/utils/shared-host-secrets-manager.test.ts +++ b/src/backend/tests/utils/shared-host-secrets-manager.test.ts @@ -4,13 +4,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const ownerDEK = crypto.randomBytes(32); const targetDEK = crypto.randomBytes(32); -type SecretRow = Record & { - id: number; - hostAccessId: number; - targetUserId: string; - protocol: string; -}; - const state = vi.hoisted(() => ({ hosts: new Map>(), credentials: new Map>(), @@ -142,6 +135,7 @@ function baseHost(overrides: Record = {}) { keyPassword: null, keyType: null, credentialId: null, + shareSshAuth: false, enableSsh: true, enableRdp: false, enableVnc: false, @@ -173,26 +167,29 @@ beforeEach(() => { }); describe("SharedHostSecretsManager", () => { - it("snapshots an inline-password SSH host and the target can decrypt it", async () => { + it("keeps an inline-password SSH host private by default", async () => { state.hosts.set(42, baseHost()); await manager.snapshotForUser(7, 42, "target", "owner"); - expect(state.secretRows).toHaveLength(1); - const row = state.secretRows[0]; - expect(row.protocol).toBe("ssh"); - expect(row.sourceType).toBe("inline"); - expect(row.encryptedPassword).not.toBe("hunter2"); + expect(state.secretRows).toHaveLength(0); + expect(await manager.getSecretForUser(42, "target", "ssh")).toBeNull(); + }); - const secret = await manager.getSecretForUser(42, "target", "ssh"); - expect(secret).toMatchObject({ + it("snapshots inline SSH authentication when the owner opts in", async () => { + state.hosts.set(42, baseHost({ shareSshAuth: true })); + + await manager.snapshotForUser(7, 42, "target", "owner"); + + expect(state.secretRows.map((row) => row.protocol)).toEqual(["ssh"]); + expect(await manager.getSecretForUser(42, "target", "ssh")).toMatchObject({ username: "root", authType: "password", password: "hunter2", }); }); - it("snapshots every enabled protocol from credential and inline sources", async () => { + it("snapshots opted-in SSH credential auth alongside enabled non-SSH protocols", async () => { state.credentials.set(123, { id: 123, userId: "owner", @@ -209,6 +206,7 @@ describe("SharedHostSecretsManager", () => { baseHost({ authType: "credential", credentialId: 123, + shareSshAuth: true, password: null, enableRdp: true, rdpUser: "rdp-admin", @@ -228,8 +226,7 @@ describe("SharedHostSecretsManager", () => { "telnet", ]); - const ssh = await manager.getSecretForUser(42, "target", "ssh"); - expect(ssh).toMatchObject({ + expect(await manager.getSecretForUser(42, "target", "ssh")).toMatchObject({ username: "cred-user", authType: "key", key: "PRIVATE-KEY", @@ -253,28 +250,28 @@ describe("SharedHostSecretsManager", () => { }); it("produces no snapshot rows for secret-less auth types", async () => { - state.hosts.set(42, baseHost({ authType: "opkssh", password: null })); + state.hosts.set( + 42, + baseHost({ + authType: "opkssh", + password: null, + shareSshAuth: true, + }), + ); await manager.snapshotForUser(7, 42, "target", "owner"); expect(state.secretRows).toHaveLength(0); }); - it("removes stale protocol rows on re-snapshot", async () => { - state.hosts.set( - 42, - baseHost({ - enableRdp: true, - rdpUser: "rdp-admin", - rdpPassword: "rdp-pass", - }), - ); + it("removes the SSH snapshot when the owner disables sharing", async () => { + state.hosts.set(42, baseHost({ shareSshAuth: true })); await manager.snapshotForUser(7, 42, "target", "owner"); - expect(state.secretRows).toHaveLength(2); + expect(state.secretRows).toHaveLength(1); - // Owner turns RDP off; the RDP snapshot must disappear. + // Owner makes SSH authentication private again. state.hosts.set(42, baseHost()); await manager.snapshotForUser(7, 42, "target", "owner"); - expect(state.secretRows.map((row) => row.protocol)).toEqual(["ssh"]); + expect(state.secretRows).toHaveLength(0); }); it("fails fast when a participant has no DEK", async () => { @@ -286,7 +283,14 @@ describe("SharedHostSecretsManager", () => { }); it("cannot be decrypted with the wrong DEK", async () => { - state.hosts.set(42, baseHost()); + state.hosts.set( + 42, + baseHost({ + enableRdp: true, + rdpUser: "rdp-admin", + rdpPassword: "rdp-pass", + }), + ); await manager.snapshotForUser(7, 42, "target", "owner"); const row = state.secretRows[0]; @@ -294,14 +298,21 @@ describe("SharedHostSecretsManager", () => { FieldCrypto.decryptField( row.encryptedPassword as string, ownerDEK, - "shared-7-target-ssh", + "shared-7-target-rdp", "password", ), ).toThrow(); }); it("resyncHost re-snapshots direct grants and role members", async () => { - state.hosts.set(42, baseHost()); + state.hosts.set( + 42, + baseHost({ + enableRdp: true, + rdpUser: "rdp-admin", + rdpPassword: "rdp-pass", + }), + ); state.accessToHost = new Map([ [1, 42], [2, 42], @@ -322,16 +333,30 @@ describe("SharedHostSecretsManager", () => { [2, "member-1"], ]); - // Owner rotates the inline password; resync updates the copies. - state.hosts.set(42, baseHost({ password: "rotated" })); + // Owner rotates the non-SSH password; resync updates those copies. + state.hosts.set( + 42, + baseHost({ + enableRdp: true, + rdpUser: "rdp-admin", + rdpPassword: "rotated", + }), + ); await manager.resyncHost(42); - const secret = await manager.getSecretForUser(42, "target", "ssh"); + const secret = await manager.getSecretForUser(42, "target", "rdp"); expect(secret?.password).toBe("rotated"); }); it("snapshotForRoleMember fans out from role grants", async () => { - state.hosts.set(42, baseHost()); + state.hosts.set( + 42, + baseHost({ + enableRdp: true, + rdpUser: "rdp-admin", + rdpPassword: "rdp-pass", + }), + ); state.accessToHost = new Map([[2, 42]]); state.grants = [{ id: 2, hostId: 42, userId: null, roleId: 9 }]; @@ -341,7 +366,7 @@ describe("SharedHostSecretsManager", () => { expect(state.secretRows[0]).toMatchObject({ hostAccessId: 2, targetUserId: "member-1", - protocol: "ssh", + protocol: "rdp", }); }); }); diff --git a/src/backend/tests/utils/system-secret-crypto.test.ts b/src/backend/tests/utils/system-secret-crypto.test.ts new file mode 100644 index 0000000..df5a1c1 --- /dev/null +++ b/src/backend/tests/utils/system-secret-crypto.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import crypto from "crypto"; + +const systemKey = crypto.randomBytes(32); +const getEncryptionKey = vi.hoisted(() => vi.fn()); + +vi.mock("../../utils/system-crypto.js", () => ({ + SystemCrypto: { getInstance: () => ({ getEncryptionKey }) }, +})); + +const { + decryptSsoConfigSecrets, + decryptSystemSecret, + encryptSsoConfigSecrets, + encryptSystemSecret, + isSystemEncrypted, + SSO_SECRET_FIELDS, +} = await import("../../utils/system-secret-crypto.js"); + +beforeEach(() => { + getEncryptionKey.mockReset(); + getEncryptionKey.mockResolvedValue(systemKey); +}); + +describe("system secret encryption", () => { + it("round-trips a secret", async () => { + const sealed = await encryptSystemSecret("s3cr3t-client-secret"); + + expect(sealed).not.toContain("s3cr3t"); + expect(isSystemEncrypted(sealed)).toBe(true); + await expect(decryptSystemSecret(sealed)).resolves.toBe( + "s3cr3t-client-secret", + ); + }); + + it("produces a different ciphertext each time", async () => { + const a = await encryptSystemSecret("same"); + const b = await encryptSystemSecret("same"); + + // Random IV per call, so identical secrets are not identifiable. + expect(a).not.toBe(b); + await expect(decryptSystemSecret(a)).resolves.toBe("same"); + await expect(decryptSystemSecret(b)).resolves.toBe("same"); + }); + + it("does not double-encrypt an already sealed value", async () => { + const once = await encryptSystemSecret("value"); + const twice = await encryptSystemSecret(once); + + expect(twice).toBe(once); + }); + + it("leaves empty values alone", async () => { + await expect(encryptSystemSecret("")).resolves.toBe(""); + await expect(decryptSystemSecret("")).resolves.toBe(""); + }); + + it("detects tampering", async () => { + const sealed = await encryptSystemSecret("value"); + const parts = sealed.replace("sysenc:v1:", "").split(":"); + const flipped = Buffer.from(parts[2], "base64"); + flipped[0] ^= 0xff; + const tampered = `sysenc:v1:${parts[0]}:${parts[1]}:${flipped.toString("base64")}`; + + // GCM auth tag must reject a modified payload rather than return garbage. + await expect(decryptSystemSecret(tampered)).rejects.toThrow(); + }); + + it("rejects a malformed sealed value", async () => { + await expect( + decryptSystemSecret("sysenc:v1:only-one-part"), + ).rejects.toThrow(/Malformed/); + }); +}); + +describe("legacy compatibility", () => { + it("decodes values written by the old base64 scheme", async () => { + const legacy = `encoded:${Buffer.from("old-secret").toString("base64")}`; + + // Must keep working: an existing install cannot be locked out of SSO login + // just because the storage format changed. + await expect(decryptSystemSecret(legacy)).resolves.toBe("old-secret"); + }); + + it("decodes the mislabelled 'encrypted:' variant too", async () => { + const legacy = `encrypted:${Buffer.from("old-secret").toString("base64")}`; + + await expect(decryptSystemSecret(legacy)).resolves.toBe("old-secret"); + }); + + it("passes through a value that was never encoded", async () => { + await expect(decryptSystemSecret("plain-secret")).resolves.toBe( + "plain-secret", + ); + }); + + it("upgrades a legacy value on the next write", async () => { + const legacy = `encoded:${Buffer.from("old-secret").toString("base64")}`; + const plaintext = await decryptSystemSecret(legacy); + const sealed = await encryptSystemSecret(plaintext); + + expect(isSystemEncrypted(sealed)).toBe(true); + await expect(decryptSystemSecret(sealed)).resolves.toBe("old-secret"); + }); +}); + +describe("SSO provider config", () => { + it("seals only the secret fields", async () => { + const sealed = await encryptSsoConfigSecrets({ + client_id: "termix", + client_secret: "shhh", + bindPassword: "ldap-pw", + issuer_url: "https://idp.example", + }); + + expect(sealed.client_id).toBe("termix"); + expect(sealed.issuer_url).toBe("https://idp.example"); + expect(isSystemEncrypted(sealed.client_secret as string)).toBe(true); + expect(isSystemEncrypted(sealed.bindPassword as string)).toBe(true); + }); + + it("round-trips a whole config", async () => { + const original = { + client_id: "termix", + client_secret: "shhh", + bindPassword: "ldap-pw", + }; + + const restored = await decryptSsoConfigSecrets( + await encryptSsoConfigSecrets(original), + ); + + expect(restored).toEqual(original); + }); + + it("covers both secret fields", () => { + expect([...SSO_SECRET_FIELDS]).toEqual(["client_secret", "bindPassword"]); + }); + + it("leaves a config without secrets untouched", async () => { + const config = { client_id: "termix", scopes: "openid" }; + + await expect(encryptSsoConfigSecrets(config)).resolves.toEqual(config); + await expect(decryptSsoConfigSecrets(config)).resolves.toEqual(config); + }); + + it("does not let one unreadable secret take down the provider", async () => { + const restored = await decryptSsoConfigSecrets({ + client_id: "termix", + client_secret: "sysenc:v1:bad", + }); + + // The rest of the config survives; login fails later with a clearer error. + expect(restored.client_id).toBe("termix"); + expect(restored.client_secret).toBe("sysenc:v1:bad"); + }); +}); diff --git a/src/backend/tests/utils/trusted-proxy-auth.test.ts b/src/backend/tests/utils/trusted-proxy-auth.test.ts new file mode 100644 index 0000000..34b06d3 --- /dev/null +++ b/src/backend/tests/utils/trusted-proxy-auth.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { + getTrustedProxyAuthConfig, + isTrustedProxyAddress, + parseTrustedProxyRoleMap, + resolveTrustedProxyRoles, +} from "../../utils/trusted-proxy-auth.js"; + +describe("trusted proxy authentication config", () => { + it("requires an explicit proxy allowlist and role map", () => { + expect(() => + getTrustedProxyAuthConfig({ TRUSTED_PROXY_AUTH_ENABLED: "true" }), + ).toThrow(/requires/); + expect(() => + getTrustedProxyAuthConfig({ + TRUSTED_PROXY_AUTH_ENABLED: "true", + TRUSTED_PROXY_AUTH_TRUSTED_PROXIES: "not-a-cidr", + TRUSTED_PROXY_AUTH_ROLE_MAP: '{"operators":"user"}', + }), + ).toThrow(/Invalid trusted proxy/); + }); + + it("matches exact addresses, CIDRs, and IPv4-mapped addresses", () => { + const trusted = ["10.20.0.0/16", "2001:db8::/32"]; + expect(isTrustedProxyAddress("10.20.1.4", trusted)).toBe(true); + expect(isTrustedProxyAddress("::ffff:10.20.1.4", trusted)).toBe(true); + expect(isTrustedProxyAddress("10.21.1.4", trusted)).toBe(false); + expect(isTrustedProxyAddress("2001:db8::5", trusted)).toBe(true); + }); + + it("fails closed when a supplied external role is not mapped", () => { + const roleMap = parseTrustedProxyRoleMap( + JSON.stringify({ operators: ["operator"], viewers: "readonly" }), + ); + expect(resolveTrustedProxyRoles("operators, viewers", roleMap)).toEqual([ + "operator", + "readonly", + ]); + expect(resolveTrustedProxyRoles("operators, admins", roleMap)).toBeNull(); + }); +}); diff --git a/src/backend/tests/utils/user-agent-parser.test.ts b/src/backend/tests/utils/user-agent-parser.test.ts index 9e002b2..0121193 100644 --- a/src/backend/tests/utils/user-agent-parser.test.ts +++ b/src/backend/tests/utils/user-agent-parser.test.ts @@ -4,6 +4,7 @@ import { detectPlatform, parseUserAgent, generateDeviceFingerprint, + getDeviceId, } from "../../utils/user-agent-parser.js"; function reqWith(headers: Record): Request { @@ -98,50 +99,81 @@ describe("parseUserAgent", () => { }); describe("generateDeviceFingerprint", () => { - it("is stable across minor browser version bumps on web", () => { - const a = generateDeviceFingerprint({ - type: "web", - browser: "Chrome", - version: "120.5", - os: "Windows 10/11", - deviceInfo: "Chrome 120.5 on Windows 10/11", - }); - const b = generateDeviceFingerprint({ - type: "web", - browser: "Chrome", - version: "120.9", - os: "Windows 10/11", - deviceInfo: "Chrome 120.9 on Windows 10/11", - }); + it("is stable for the same client device id", () => { + const a = generateDeviceFingerprint( + { + type: "web", + browser: "Chrome", + version: "120.5", + os: "Windows 10/11", + deviceInfo: "Chrome 120.5 on Windows 10/11", + }, + "a".repeat(64), + ); + const b = generateDeviceFingerprint( + { + type: "web", + browser: "Chrome", + version: "121.9", + os: "Windows 10/11", + deviceInfo: "Chrome 121.9 on Windows 10/11", + }, + "a".repeat(64), + ); expect(a).toBe(b); }); - it("differs across major browser versions on web", () => { - const a = generateDeviceFingerprint({ - type: "web", - browser: "Chrome", - version: "120.0", - os: "Windows 10/11", - deviceInfo: "", - }); - const b = generateDeviceFingerprint({ - type: "web", - browser: "Chrome", - version: "121.0", - os: "Windows 10/11", - deviceInfo: "", - }); + it("differs for two clients on the same platform", () => { + const a = generateDeviceFingerprint( + { + type: "desktop", + browser: "Termix Desktop", + version: "2.7.0", + os: "Linux", + deviceInfo: "", + }, + "a".repeat(64), + ); + const b = generateDeviceFingerprint( + { + type: "desktop", + browser: "Termix Desktop", + version: "2.7.0", + os: "Linux", + deviceInfo: "", + }, + "b".repeat(64), + ); expect(a).not.toBe(b); }); - it("produces a 64-char hex sha256 digest", () => { - const fp = generateDeviceFingerprint({ - type: "desktop", - browser: "Termix Desktop", - version: "2.3.1", - os: "macOS", - deviceInfo: "", - }); - expect(fp).toMatch(/^[0-9a-f]{64}$/); + it("does not trust clients without a device id", () => { + const fp = generateDeviceFingerprint( + { + type: "desktop", + browser: "Termix Desktop", + version: "2.3.1", + os: "macOS", + deviceInfo: "", + }, + null, + ); + expect(fp).toBeNull(); + }); +}); + +describe("getDeviceId", () => { + it("accepts a 256-bit hex device id", () => { + const deviceId = "a".repeat(64); + expect(getDeviceId(reqWith({ "x-termix-device-id": deviceId }))).toBe( + deviceId, + ); + }); + + it("rejects missing or malformed device ids", () => { + expect(getDeviceId(reqWith({}))).toBeNull(); + expect( + getDeviceId(reqWith({ "x-termix-device-id": "shared-linux" })), + ).toBeNull(); }); }); diff --git a/src/backend/utils/alert-trigger.ts b/src/backend/utils/alert-trigger.ts index c716465..2098bec 100644 --- a/src/backend/utils/alert-trigger.ts +++ b/src/backend/utils/alert-trigger.ts @@ -11,14 +11,23 @@ export async function triggerLoginAlert( ): Promise { try { const token = await SystemCrypto.getInstance().getInternalAuthToken(); - await fetch(`${METRICS_SERVICE_URL}/internal/login-alert`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-internal-auth": token, + const response = await fetch( + `${METRICS_SERVICE_URL}/internal/login-alert`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-internal-auth": token, + }, + body: JSON.stringify({ hostId, userId, sshUser, fromIp }), }, - body: JSON.stringify({ hostId, userId, sshUser, fromIp }), - }); + ); + if (!response.ok) { + const details = await response.text(); + throw new Error( + `Metrics service returned ${response.status}${details ? `: ${details}` : ""}`, + ); + } } catch (err) { sshLogger.warn("Failed to trigger login alert", { operation: "login_alert_trigger_error", diff --git a/src/backend/utils/analytics.ts b/src/backend/utils/analytics.ts new file mode 100644 index 0000000..703ba08 --- /dev/null +++ b/src/backend/utils/analytics.ts @@ -0,0 +1,153 @@ +import { getErrorMessage } from "./error-message.js"; +import crypto from "crypto"; +import axios from "axios"; +import { sql } from "drizzle-orm"; +import { users, hosts, recentActivity } from "../database/db/schema.js"; +import { + createCurrentSettingsRepository, + createCurrentRepositoryContext, +} from "../database/repositories/factory.js"; +import { Logger } from "./logger.js"; + +export const analyticsLogger = new Logger("ANALYTICS", "๐Ÿ“ˆ", "#06b6d4"); + +const FEATURE_ACTIVITY_TYPES = [ + "terminal", + "file_manager", + "tunnel", + "docker", + "telnet", + "vnc", + "rdp", + "server_stats", +] as const; + +const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com"; +const HEARTBEAT_INTERVAL_MS = 24 * 60 * 60 * 1000; + +export function getTelemetryEnvOverride(): boolean | null { + const envVal = process.env.ENABLE_TELEMETRY; + if (envVal === undefined) return null; + const normalized = envVal.trim().toLowerCase(); + if (normalized === "") return null; + return normalized === "true"; +} + +export async function isAnalyticsEnabled(): Promise { + const override = getTelemetryEnvOverride(); + if (override !== null) return override; + + return createCurrentSettingsRepository().getBoolean( + "analytics_enabled", + true, + ); +} + +export async function getOrCreateInstanceId(): Promise { + const settings = createCurrentSettingsRepository(); + const existing = await settings.get("analytics_instance_id"); + if (existing) return existing; + + const id = crypto.randomUUID(); + await settings.set("analytics_instance_id", id); + return id; +} + +function getAppVersion(): string { + return process.env.VERSION || "unknown"; +} + +async function collectFeatureUsage(): Promise> { + const since = new Date(Date.now() - HEARTBEAT_INTERVAL_MS).toISOString(); + const db = createCurrentRepositoryContext().drizzle; + + const rows = await db + .select({ + type: recentActivity.type, + count: sql`count(*)`, + }) + .from(recentActivity) + .where(sql`${recentActivity.timestamp} >= ${since}`) + .groupBy(recentActivity.type); + + const counts = new Map(rows.map((row) => [row.type, Number(row.count)])); + const usage: Record = {}; + for (const type of FEATURE_ACTIVITY_TYPES) { + usage[`used_${type}`] = counts.get(type) ?? 0; + } + return usage; +} + +async function collectCounts(): Promise<{ + userCount: number; + hostCount: number; +}> { + const db = createCurrentRepositoryContext().drizzle; + + const [userRows, hostRows] = await Promise.all([ + db.select({ count: sql`count(*)` }).from(users), + db.select({ count: sql`count(*)` }).from(hosts), + ]); + + return { + userCount: Number(userRows[0]?.count ?? 0), + hostCount: Number(hostRows[0]?.count ?? 0), + }; +} + +export async function collectAndSendHeartbeat(): Promise { + const apiKey = process.env.POSTHOG_API_KEY; + if (!apiKey) return; + + try { + if (!(await isAnalyticsEnabled())) return; + + const instanceId = await getOrCreateInstanceId(); + const { userCount, hostCount } = await collectCounts(); + const featureUsage = await collectFeatureUsage(); + + await axios.post( + `${POSTHOG_HOST}/capture/`, + { + api_key: apiKey, + event: "instance_heartbeat", + distinct_id: instanceId, + properties: { + version: getAppVersion(), + user_count: userCount, + host_count: hostCount, + ...featureUsage, + }, + }, + { timeout: 10000 }, + ); + + analyticsLogger.info("Sent daily usage heartbeat", { + operation: "analytics_heartbeat_sent", + }); + } catch (err) { + analyticsLogger.warn("Failed to send usage heartbeat", { + operation: "analytics_heartbeat_failed", + error: getErrorMessage(err), + }); + } +} + +export function startAnalyticsHeartbeat(): void { + if (getTelemetryEnvOverride() === false) { + analyticsLogger.info("Telemetry disabled by ENABLE_TELEMETRY", { + operation: "analytics_disabled_by_env", + }); + return; + } + + if (!process.env.POSTHOG_API_KEY) { + analyticsLogger.info("Analytics disabled: POSTHOG_API_KEY not set", { + operation: "analytics_disabled_no_key", + }); + return; + } + + void collectAndSendHeartbeat(); + setInterval(() => void collectAndSendHeartbeat(), HEARTBEAT_INTERVAL_MS); +} diff --git a/src/backend/utils/audit-export.ts b/src/backend/utils/audit-export.ts new file mode 100644 index 0000000..5f84891 --- /dev/null +++ b/src/backend/utils/audit-export.ts @@ -0,0 +1,70 @@ +import type { AuditLogRecord } from "../database/repositories/audit-log-repository.js"; + +/** Column order for CSV export; also the header row. */ +const COLUMNS = [ + "id", + "timestamp", + "username", + "userId", + "action", + "resourceType", + "resourceId", + "resourceName", + "success", + "ipAddress", + "userAgent", + "errorMessage", + "details", +] as const; + +/** + * RFC 4180 field escaping. + * + * The leading-character guard is not part of RFC 4180: a field starting with + * `=`, `+`, `-` or `@` is treated as a formula by spreadsheet software, so an + * audit entry containing an attacker-chosen resource name could execute on + * open. Prefixing with a single quote neutralises that while keeping the value + * readable. + */ +export function escapeCsvField(value: unknown): string { + if (value === null || value === undefined) return ""; + + let text = typeof value === "boolean" ? String(value) : String(value); + if (/^[=+\-@\t\r]/.test(text)) { + text = `'${text}`; + } + + if (/[",\n\r]/.test(text)) { + return `"${text.replace(/"/g, '""')}"`; + } + return text; +} + +export function toCsv(rows: AuditLogRecord[]): string { + const lines = [COLUMNS.join(",")]; + for (const row of rows) { + lines.push( + COLUMNS.map((column) => + escapeCsvField((row as Record)[column]), + ).join(","), + ); + } + // Trailing newline so the file ends cleanly when appended to or concatenated. + return `${lines.join("\n")}\n`; +} + +/** + * Newline-delimited JSON: one entry per line, which is what log shippers and + * SIEM bulk endpoints expect, and which streams without holding the whole set. + */ +export function toNdjson(rows: AuditLogRecord[]): string { + return ( + rows.map((row) => JSON.stringify(row)).join("\n") + + (rows.length ? "\n" : "") + ); +} + +export function exportFilename(format: "csv" | "ndjson", now: Date): string { + const stamp = now.toISOString().slice(0, 19).replace(/[:T]/g, "-"); + return `termix-audit-${stamp}.${format === "csv" ? "csv" : "ndjson"}`; +} diff --git a/src/backend/utils/audit-forwarder.ts b/src/backend/utils/audit-forwarder.ts new file mode 100644 index 0000000..8b90624 --- /dev/null +++ b/src/backend/utils/audit-forwarder.ts @@ -0,0 +1,132 @@ +import { safeOutboundFetch } from "./safe-outbound-fetch.js"; +import { databaseLogger } from "./logger.js"; +import type { AuditLogParams } from "./audit-logger.js"; + +export const AUDIT_FORWARD_URL_ENV = "AUDIT_LOG_FORWARD_URL"; +export const AUDIT_FORWARD_TOKEN_ENV = "AUDIT_LOG_FORWARD_TOKEN"; + +/** + * How many consecutive failures before the forwarder stops complaining on every + * entry. It keeps trying โ€” this only throttles the log noise, and it reports + * again once delivery recovers. + */ +const QUIET_AFTER_FAILURES = 5; + +let consecutiveFailures = 0; +let quietened = false; + +export interface AuditForwardTarget { + url: string; + token?: string; +} + +export function auditForwardTarget( + env: NodeJS.ProcessEnv = process.env, +): AuditForwardTarget | null { + const url = env[AUDIT_FORWARD_URL_ENV]?.trim(); + if (!url) return null; + const token = env[AUDIT_FORWARD_TOKEN_ENV]?.trim(); + return token ? { url, token } : { url }; +} + +/** The wire shape: one JSON object per entry, matching the export's NDJSON. */ +export function forwardPayload( + entry: AuditLogParams, + now: Date, +): Record { + return { + timestamp: now.toISOString(), + userId: entry.userId, + username: entry.username, + action: entry.action, + resourceType: entry.resourceType, + resourceId: entry.resourceId ?? null, + resourceName: entry.resourceName ?? null, + success: entry.success, + ipAddress: entry.ipAddress ?? null, + userAgent: entry.userAgent ?? null, + errorMessage: entry.errorMessage ?? null, + details: entry.details ?? null, + }; +} + +/** Exposed for tests; forwarding state is process-wide otherwise. */ +export function resetAuditForwarderState(): void { + consecutiveFailures = 0; + quietened = false; +} + +/** + * Ships one entry to the configured collector. + * + * Never throws and never blocks the audited operation: a SIEM being unreachable + * must not stop Termix from recording locally, which stays the source of truth. + * Delivery goes through safeOutboundFetch so a misconfigured URL cannot be used + * to probe the internal network. + */ +export async function forwardAuditEntry( + entry: AuditLogParams, + now: Date = new Date(), + env: NodeJS.ProcessEnv = process.env, +): Promise { + const target = auditForwardTarget(env); + if (!target) return false; + + try { + const response = await safeOutboundFetch(target.url, { + method: "POST", + headers: { + "Content-Type": "application/x-ndjson", + ...(target.token ? { Authorization: `Bearer ${target.token}` } : {}), + }, + body: `${JSON.stringify(forwardPayload(entry, now))}\n`, + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) { + noteFailure(`collector returned ${response.status}`, entry.action); + return false; + } + + noteSuccess(); + return true; + } catch (error) { + noteFailure( + error instanceof Error ? error.message : String(error), + entry.action, + ); + return false; + } +} + +function noteFailure(reason: string, action: string): void { + consecutiveFailures++; + + if (quietened) return; + + databaseLogger.warn("Failed to forward audit entry", { + operation: "audit_forward_failed", + action, + reason, + consecutiveFailures, + }); + + if (consecutiveFailures >= QUIET_AFTER_FAILURES) { + quietened = true; + databaseLogger.warn( + `Audit forwarding has failed ${consecutiveFailures} times; suppressing further messages until it recovers`, + { operation: "audit_forward_suppressed" }, + ); + } +} + +function noteSuccess(): void { + if (quietened) { + databaseLogger.info("Audit forwarding recovered", { + operation: "audit_forward_recovered", + afterFailures: consecutiveFailures, + }); + } + consecutiveFailures = 0; + quietened = false; +} diff --git a/src/backend/utils/audit-logger.ts b/src/backend/utils/audit-logger.ts index c48c038..235302c 100644 --- a/src/backend/utils/audit-logger.ts +++ b/src/backend/utils/audit-logger.ts @@ -1,5 +1,23 @@ import type { Request } from "express"; -import { createCurrentAuditLogRepository } from "../database/repositories/factory.js"; +import { forwardAuditEntry } from "./audit-forwarder.js"; +import { + createCurrentAuditLogRepository, + createCurrentUserRepository, +} from "../database/repositories/factory.js"; +import { getClientIp } from "./request-origin.js"; + +/** + * Resolves the display name to store alongside the entry. It is denormalised on + * purpose: the record has to stay readable after the account is gone. + */ +export async function getAuditUsername(userId: string): Promise { + try { + const actor = await createCurrentUserRepository().findById(userId); + return actor?.username ?? userId; + } catch { + return userId; + } +} export interface AuditLogParams { userId: string; @@ -16,6 +34,10 @@ export interface AuditLogParams { } export async function logAudit(params: AuditLogParams): Promise { + // Local storage is the source of truth and runs first; forwarding is a copy + // and must never delay or fail the audited operation. + void forwardAuditEntry(params).catch(() => {}); + try { await createCurrentAuditLogRepository().create({ userId: params.userId, @@ -39,11 +61,6 @@ export function getRequestMeta(req: Request): { ipAddress: string; userAgent: string; } { - const forwarded = req.headers["x-forwarded-for"]; - const ipAddress = - (Array.isArray(forwarded) ? forwarded[0] : forwarded?.split(",")[0]) || - req.ip || - ""; const userAgent = (req.headers["user-agent"] as string) || ""; - return { ipAddress, userAgent }; + return { ipAddress: getClientIp(req), userAgent }; } diff --git a/src/backend/utils/audit-retention-migration.ts b/src/backend/utils/audit-retention-migration.ts new file mode 100644 index 0000000..ef01916 --- /dev/null +++ b/src/backend/utils/audit-retention-migration.ts @@ -0,0 +1,221 @@ +import { databaseLogger } from "./logger.js"; + +export interface MigratableSqlite { + exec(sql: string): unknown; + prepare(sql: string): { + all(...params: unknown[]): unknown[]; + get(...params: unknown[]): unknown; + run(...params: unknown[]): unknown; + }; +} + +interface ForeignKeyRow { + table?: string; + from?: string; + on_delete?: string; +} + +interface RetainedTable { + name: string; + /** Column list for the copy, in the order the rebuilt table declares them. */ + columns: string[]; + createSql: string; +} + +/** + * `audit_logs` already denormalises `username`, so nulling `user_id` still + * leaves a record of who acted. `session_recordings` does not, which is why the + * column is added and backfilled before its foreign key is relaxed โ€” otherwise + * relaxing it would trade deleted evidence for anonymous evidence. + */ +const AUDIT_LOGS: RetainedTable = { + name: "audit_logs", + columns: [ + "id", + "user_id", + "username", + "action", + "resource_type", + "resource_id", + "resource_name", + "details", + "ip_address", + "user_agent", + "success", + "error_message", + "timestamp", + ], + createSql: ` + CREATE TABLE audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT, + username TEXT NOT NULL, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + resource_name TEXT, + details TEXT, + ip_address TEXT, + user_agent TEXT, + success INTEGER NOT NULL, + error_message TEXT, + timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL + ); + `, +}; + +const SESSION_RECORDINGS: RetainedTable = { + name: "session_recordings", + columns: [ + "id", + "host_id", + "user_id", + "username", + "access_id", + "started_at", + "ended_at", + "duration", + "commands", + "dangerous_actions", + "recording_path", + "protocol", + "format", + "terminated_by_owner", + "termination_reason", + ], + createSql: ` + CREATE TABLE session_recordings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT, + username TEXT, + access_id INTEGER, + started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + ended_at TEXT, + duration INTEGER, + commands TEXT, + dangerous_actions TEXT, + recording_path TEXT, + protocol TEXT NOT NULL DEFAULT 'ssh', + format TEXT NOT NULL DEFAULT 'text', + terminated_by_owner INTEGER DEFAULT 0, + termination_reason TEXT, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL, + FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL + ); + `, +}; + +const RETAINED_TABLES = [AUDIT_LOGS, SESSION_RECORDINGS]; + +export function userDeleteIsDestructive( + sqlite: MigratableSqlite, + table: string, +): boolean { + let rows: ForeignKeyRow[]; + try { + rows = sqlite + .prepare(`PRAGMA foreign_key_list(${table})`) + .all() as ForeignKeyRow[]; + } catch { + // Table absent on a fresh database; it is created in the target shape. + return false; + } + + return rows.some( + (row) => + row.table === "users" && + row.from === "user_id" && + (row.on_delete ?? "").toUpperCase() === "CASCADE", + ); +} + +function hasColumn( + sqlite: MigratableSqlite, + table: string, + column: string, +): boolean { + try { + sqlite.prepare(`SELECT "${column}" FROM ${table} LIMIT 1`).get(); + return true; + } catch { + return false; + } +} + +/** + * Gives session_recordings a username before its user_id can become null, so + * existing rows stay attributable. + */ +function ensureRecordingUsername(sqlite: MigratableSqlite): void { + if (hasColumn(sqlite, "session_recordings", "username")) return; + + sqlite.exec(`ALTER TABLE session_recordings ADD COLUMN username TEXT;`); + sqlite.exec(` + UPDATE session_recordings + SET username = (SELECT username FROM users WHERE users.id = session_recordings.user_id) + WHERE username IS NULL; + `); +} + +/** + * SQLite cannot alter a foreign key in place, so the table is copied into a new + * one with the intended constraint and swapped. Foreign keys must be off. + */ +function rebuildTable(sqlite: MigratableSqlite, table: RetainedTable): void { + const columns = table.columns.join(", "); + const temp = `${table.name}_retained`; + + sqlite.exec(table.createSql.replace(table.name, temp)); + sqlite.exec( + `INSERT INTO ${temp} (${columns}) SELECT ${columns} FROM ${table.name};`, + ); + sqlite.exec(`DROP TABLE ${table.name};`); + sqlite.exec(`ALTER TABLE ${temp} RENAME TO ${table.name};`); +} + +/** + * Turns ON DELETE CASCADE into ON DELETE SET NULL for the tables that have to + * outlive the account they reference. Idempotent. + */ +export function migrateAuditRetention(sqlite: MigratableSqlite): string[] { + const migrated: string[] = []; + + for (const table of RETAINED_TABLES) { + if (!userDeleteIsDestructive(sqlite, table.name)) continue; + + try { + if (table.name === "session_recordings") { + ensureRecordingUsername(sqlite); + } + + sqlite.exec("PRAGMA foreign_keys = OFF"); + sqlite.exec("BEGIN TRANSACTION"); + rebuildTable(sqlite, table); + sqlite.exec("COMMIT"); + + migrated.push(table.name); + databaseLogger.info(`${table.name} now survives user deletion`, { + operation: "audit_retention_migration", + table: table.name, + }); + } catch (error) { + try { + sqlite.exec("ROLLBACK"); + } catch { + // no transaction open + } + databaseLogger.warn(`Could not migrate ${table.name} retention`, { + operation: "audit_retention_migration_failed", + table: table.name, + error: error instanceof Error ? error.message : String(error), + }); + } finally { + sqlite.exec("PRAGMA foreign_keys = ON"); + } + } + + return migrated; +} diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 6d2a29c..f920071 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "./error-message.js"; import jwt from "jsonwebtoken"; import crypto from "crypto"; import { UserKeyManager } from "./user-keys.js"; @@ -193,7 +194,7 @@ class AuthManager { databaseLogger.error("Lazy encryption migration failed", error, { operation: "lazy_encryption_migration_error", userId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -248,7 +249,7 @@ class AuthManager { operation: "session_data_key_migrate_failed", userId: payload.userId, sessionId: payload.sessionId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -391,7 +392,7 @@ class AuthManager { } catch (error) { databaseLogger.warn("JWT verification failed", { operation: "jwt_verify_failed", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), errorName: error instanceof Error ? error.name : "Unknown", }); return null; @@ -659,7 +660,7 @@ class AuthManager { databaseLogger.warn("Failed to update API key lastUsedAt", { operation: "api_key_update_last_used", keyId: matchedKey!.id, - error: err instanceof Error ? err.message : "Unknown", + error: getErrorMessage(err, "Unknown"), }); }); @@ -767,7 +768,7 @@ class AuthManager { databaseLogger.warn("Failed to update session lastActiveAt", { operation: "session_update_last_active", sessionId: payload.sessionId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); }); } catch (error) { diff --git a/src/backend/utils/auto-ssl-setup.ts b/src/backend/utils/auto-ssl-setup.ts index 7550e8e..240f6e3 100644 --- a/src/backend/utils/auto-ssl-setup.ts +++ b/src/backend/utils/auto-ssl-setup.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "./error-message.js"; import { execSync } from "child_process"; import { promises as fs } from "fs"; import path from "path"; @@ -170,7 +171,7 @@ IP.3 = 0.0.0.0 await this.logCertificateInfo(); } catch (error) { throw new Error( - `SSL certificate generation failed: ${error instanceof Error ? error.message : "Unknown error"}`, + `SSL certificate generation failed: ${getErrorMessage(error)}`, { cause: error }, ); } @@ -214,7 +215,7 @@ IP.3 = 0.0.0.0 } catch (error) { systemLogger.warn("Could not retrieve certificate information", { operation: "ssl_cert_info_error", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -224,7 +225,7 @@ IP.3 = 0.0.0.0 const keyPath = this.KEY_FILE; const sslEnvVars = { - ENABLE_SSL: "false", + ENABLE_SSL: "true", SSL_PORT: process.env.SSL_PORT || "8443", SSL_CERT_PATH: certPath, SSL_KEY_PATH: keyPath, diff --git a/src/backend/utils/compression-config.ts b/src/backend/utils/compression-config.ts new file mode 100644 index 0000000..0a003bc --- /dev/null +++ b/src/backend/utils/compression-config.ts @@ -0,0 +1,50 @@ +import compression from "compression"; +import { type Request, type RequestHandler, type Response } from "express"; + +/** + * Below this, compressing costs more than it saves. + * + * The default is 1KB; this is slightly higher because almost every response + * under a few KB here is a small status or preference object where the CPU and + * the extra headers are not worth it. The payloads that matter โ€” host lists, + * audit pages, fleet inventories โ€” are orders of magnitude above this. + */ +const MIN_RESPONSE_BYTES = 2048; + +/** + * Streaming endpoints that must not be buffered. + * + * Compression holds bytes back to build a block, which is exactly wrong for a + * response whose value is arriving incrementally: SSE heartbeats and download + * streams would stall until the buffer filled or the request ended. + */ +function isStreamingResponse(res: Response): boolean { + const contentType = String(res.getHeader("Content-Type") ?? ""); + return ( + contentType.includes("text/event-stream") || + contentType.includes("application/octet-stream") + ); +} + +/** + * gzip for JSON API responses. + * + * The host list is the reason this exists: it is one big JSON array whose rows + * repeat the same ~77 keys, so it compresses about 69x. Without this an install + * with a few thousand hosts ships tens of megabytes per refresh. + * + * Deliberately not brotli: it compresses better but costs noticeably more CPU + * per response, and the difference matters far less than the 60x that gzip + * already recovers. + */ +export function createCompressionMiddleware(): RequestHandler { + return compression({ + threshold: MIN_RESPONSE_BYTES, + filter: (req: Request, res: Response) => { + // Lets a caller opt out explicitly, which is useful when debugging. + if (req.headers["x-no-compression"]) return false; + if (isStreamingResponse(res)) return false; + return compression.filter(req, res); + }, + }); +} diff --git a/src/backend/utils/cors-config.ts b/src/backend/utils/cors-config.ts index 582d454..63a8058 100644 --- a/src/backend/utils/cors-config.ts +++ b/src/backend/utils/cors-config.ts @@ -35,6 +35,7 @@ export function createCorsMiddleware( "Authorization", "User-Agent", "X-Electron-App", + "X-Termix-Device-ID", "Cache-Control", "x-admin-target-user", ...extraHeaders, diff --git a/src/backend/utils/crypto-migration/automations-migration.ts b/src/backend/utils/crypto-migration/automations-migration.ts new file mode 100644 index 0000000..7ff0749 --- /dev/null +++ b/src/backend/utils/crypto-migration/automations-migration.ts @@ -0,0 +1,218 @@ +import type { + AutomationDefinition, + Step, + Trigger, +} from "../../../types/automations.js"; +import { AUTOMATION_DEFINITION_VERSION } from "../../../types/automations.js"; +import { databaseLogger } from "../logger.js"; +import { + createCurrentAutomationRepository, + createCurrentRepositoryContext, + createCurrentSettingsRepository, +} from "../../database/repositories/factory.js"; +import { alertRuleChannels, alertRules } from "../../database/db/schema.js"; +import { eq } from "drizzle-orm"; + +const MIGRATION_FLAG = "alert_rules_to_automations_v1"; + +export interface AutomationsMigrationResult { + migrated: number; + skipped: number; +} + +/** + * Carries existing alert rules over into automations. + * + * Each rule becomes an automation whose trigger is the rule's threshold or + * state change and whose only step notifies the channels that rule was linked + * to, which is exactly what the alert engine did with it. + * + * The alert_* tables are deliberately left in place. They are the rollback + * path, and alert_firings is user-visible history; nothing writes to them once + * the automations engine takes over. + */ +export async function runAutomationsMigration(): Promise { + const settingsRepository = createCurrentSettingsRepository(); + + try { + if ((await settingsRepository.get(MIGRATION_FLAG)) === "done") { + // Already migrated on an earlier boot; the automations engine owns + // evaluation from here, so the old one must stay quiet. + await standDownAlertEngine(); + return null; + } + + const { drizzle } = createCurrentRepositoryContext(); + const rules = await drizzle.select().from(alertRules); + + if (rules.length === 0) { + await settingsRepository.set(MIGRATION_FLAG, "done"); + await standDownAlertEngine(); + return { migrated: 0, skipped: 0 }; + } + + const repository = createCurrentAutomationRepository(); + let migrated = 0; + let skipped = 0; + + for (const rule of rules) { + const trigger = triggerForRule(rule); + if (!trigger) { + skipped++; + continue; + } + + const links = await drizzle + .select({ channelId: alertRuleChannels.channelId }) + .from(alertRuleChannels) + .where(eq(alertRuleChannels.ruleId, rule.id)); + const channelIds = links.map((link) => link.channelId); + + const steps: Step[] = [ + { + id: "notify", + type: "notify", + channelIds, + title: `${rule.name}`, + body: messageTemplateFor(rule.triggerType), + severity: "warning", + }, + ]; + + const definition: AutomationDefinition = { + version: AUTOMATION_DEFINITION_VERSION, + trigger, + steps, + }; + + await repository.create({ + userId: rule.userId, + name: rule.name, + description: "Migrated from an alert rule", + enabled: !!rule.enabled, + definition: JSON.stringify(definition), + channels: channelIds, + }); + migrated++; + } + + await settingsRepository.set(MIGRATION_FLAG, "done"); + await standDownAlertEngine(); + + if (migrated > 0) { + databaseLogger.info(`Migrated ${migrated} alert rule(s) to automations`, { + operation: "automations_migration", + migrated, + skipped, + }); + } + + return { migrated, skipped }; + } catch (error) { + databaseLogger.warn("Alert rule migration failed", { + operation: "automations_migration_error", + error: error instanceof Error ? error.message : String(error), + }); + return null; + } +} + +/** + * Imported lazily: this migration runs before the metrics subsystem is loaded, + * and pulling that module in early would drag its timers in with it. + */ +async function standDownAlertEngine(): Promise { + try { + const { markAlertEngineSuperseded } = + await import("../../hosts/metrics/alert-engine.js"); + markAlertEngineSuperseded(); + } catch { + // If it cannot be loaded there is nothing running to stand down. + } +} + +type AlertRuleRecord = typeof alertRules.$inferSelect; + +function triggerForRule(rule: AlertRuleRecord): Trigger | null { + const hostSelector = + rule.hostId === null + ? ({ kind: "all" } as const) + : ({ kind: "host", hostId: rule.hostId } as const); + const cooldownMinutes = rule.cooldownMinutes ?? 15; + + switch (rule.triggerType) { + case "cpu_threshold": + case "memory_threshold": + case "disk_threshold": { + const path = + rule.triggerType === "cpu_threshold" + ? "cpu.percent" + : rule.triggerType === "memory_threshold" + ? "memory.percent" + : "disk.percent"; + return { + kind: "metric_threshold", + hostSelector, + metric: { path } as Extract< + Trigger, + { kind: "metric_threshold" } + >["metric"], + // The old engine only ever compared with >=. + operator: ">=", + value: rule.thresholdValue ?? 0, + forSeconds: rule.thresholdDurationSeconds ?? undefined, + cooldownMinutes, + }; + } + case "host_offline": + return { + kind: "host_status", + hostSelector, + to: "offline", + cooldownMinutes, + }; + case "host_online": + return { + kind: "host_status", + hostSelector, + to: "online", + cooldownMinutes, + }; + case "health_check_failure": + return { + kind: "health_check", + hostSelector, + to: "failing", + cooldownMinutes, + }; + case "health_check_recovery": + return { + kind: "health_check", + hostSelector, + to: "recovered", + cooldownMinutes, + }; + case "user_login": + return { + kind: "internal_event", + event: "user_login", + hostSelector, + cooldownMinutes, + }; + default: + return null; + } +} + +function messageTemplateFor(triggerType: string): string { + if (triggerType.endsWith("_threshold")) { + return "{{trigger.metric}} on host {{trigger.hostId}} is at {{trigger.value}} (threshold {{trigger.threshold}})"; + } + if (triggerType.startsWith("host_")) { + return "Host {{trigger.hostId}} is {{trigger.status}}"; + } + if (triggerType.startsWith("health_check")) { + return "Health check {{trigger.checkId}} on host {{trigger.hostId}} is {{trigger.state}}"; + } + return "Triggered on host {{trigger.hostId}}"; +} diff --git a/src/backend/utils/crypto-migration/channel-config-encryption.ts b/src/backend/utils/crypto-migration/channel-config-encryption.ts new file mode 100644 index 0000000..117ea9b --- /dev/null +++ b/src/backend/utils/crypto-migration/channel-config-encryption.ts @@ -0,0 +1,106 @@ +import { eq } from "drizzle-orm"; +import { databaseLogger } from "../logger.js"; +import { + createCurrentRepositoryContext, + createCurrentSettingsRepository, +} from "../../database/repositories/factory.js"; +import { notificationChannels } from "../../database/db/schema.js"; +import { DataCrypto } from "../data-crypto.js"; +import { FieldCrypto } from "../field-crypto.js"; + +const MIGRATION_FLAG = "notification_channel_config_encrypted_v1"; + +export interface ChannelConfigEncryptionResult { + encrypted: number; + skipped: number; +} + +/** + * Notification channel configs hold ntfy tokens, webhook auth headers and + * Discord webhook URLs, but shipped as plaintext JSON while every other secret + * in the app was field-encrypted. This encrypts the rows already on disk. + * + * Rows whose owner has no usable data key are left alone and retried on a later + * boot; the repository reads tolerate both shapes, so a partial run is safe. + */ +export async function runChannelConfigEncryptionMigration(): Promise { + const settingsRepository = createCurrentSettingsRepository(); + + try { + if ((await settingsRepository.get(MIGRATION_FLAG)) === "done") { + return null; + } + + const { drizzle } = createCurrentRepositoryContext(); + const rows = await drizzle + .select({ + id: notificationChannels.id, + userId: notificationChannels.userId, + config: notificationChannels.config, + }) + .from(notificationChannels); + + let encrypted = 0; + let skipped = 0; + let deferred = 0; + + for (const row of rows) { + if (!row.config || FieldCrypto.isEncrypted(row.config)) { + skipped++; + continue; + } + + let userDataKey: Buffer | null = null; + try { + userDataKey = DataCrypto.getUserDataKey(row.userId); + } catch { + userDataKey = null; + } + + // No usable key yet (legacy DEK pending migration); retry on a later boot. + if (!userDataKey) { + deferred++; + continue; + } + + const value = DataCrypto.encryptRecord( + "notification_channels", + { id: row.id, config: row.config }, + row.userId, + userDataKey, + ).config; + + await drizzle + .update(notificationChannels) + .set({ config: value }) + .where(eq(notificationChannels.id, row.id)); + encrypted++; + } + + // Only close the migration out once no row is still waiting on a key, so a + // boot that could not reach some users' keys retries those rows next time. + if (deferred === 0) { + await settingsRepository.set(MIGRATION_FLAG, "done"); + } + + if (encrypted > 0 || deferred > 0) { + databaseLogger.info( + `Encrypted ${encrypted} notification channel config(s)`, + { + operation: "channel_config_encryption_migration", + encrypted, + skipped, + deferred, + }, + ); + } + + return { encrypted, skipped: skipped + deferred }; + } catch (error) { + databaseLogger.warn("Notification channel config encryption failed", { + operation: "channel_config_encryption_error", + error: error instanceof Error ? error.message : String(error), + }); + return null; + } +} diff --git a/src/backend/utils/crypto-migration/legacy-share-cleanup.ts b/src/backend/utils/crypto-migration/legacy-share-cleanup.ts index 64cbb05..4fa020b 100644 --- a/src/backend/utils/crypto-migration/legacy-share-cleanup.ts +++ b/src/backend/utils/crypto-migration/legacy-share-cleanup.ts @@ -1,6 +1,10 @@ import { databaseLogger } from "../logger.js"; import { DatabaseSaveTrigger } from "../database-save-trigger.js"; import { getCurrentRepositorySqlite } from "../../database/repositories/factory.js"; +import { + needsExplicitPersist, + resolveDatabaseDialect, +} from "../../database/db/dialect.js"; interface SqliteLike { prepare(sql: string): { @@ -39,9 +43,16 @@ function dropColumnIfExists( export async function runLegacySharedCredentialCleanup(): Promise<{ columnsDropped: number; }> { - const sqlite = getCurrentRepositorySqlite() as unknown as SqliteLike; const result = { columnsDropped: 0 }; + // Nothing to clean up on an engine this application has never run on. A + // Postgres or MySQL database is created by the drizzle migrations, which have + // never emitted these legacy columns, so there is nothing to drop โ€” and the + // check itself needs a synchronous PRAGMA that only SQLite offers. + if (!needsExplicitPersist(resolveDatabaseDialect())) return result; + + const sqlite = getCurrentRepositorySqlite() as unknown as SqliteLike; + for (const [table, column] of [ ["ssh_credentials", "system_password"], ["ssh_credentials", "system_key"], diff --git a/src/backend/utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.ts b/src/backend/utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.ts new file mode 100644 index 0000000..b952dbd --- /dev/null +++ b/src/backend/utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.ts @@ -0,0 +1,150 @@ +import { getErrorMessage } from "../error-message.js"; +import { DatabaseSaveTrigger } from "../database-save-trigger.js"; +import { databaseLogger } from "../logger.js"; +import { + createCurrentSettingsRepository, + getCurrentRepositorySqlite, +} from "../../database/repositories/factory.js"; +import { + needsExplicitPersist, + resolveDatabaseDialect, +} from "../../database/db/dialect.js"; +import { SharedHostSecretsManager } from "../shared-host-secrets-manager.js"; + +const MIGRATION_FLAG = "legacy_shared_ssh_auth_opt_in_v1"; + +interface SharedHostRow { + id: number; +} + +export interface LegacySharedSshAuthOptInResult { + enabled: number; + resynced: number; + skipped: number; +} + +/** + * Before SSH authentication became owner-controlled, every shared host shared + * its SSH auth automatically. Preserve that behavior for hosts which already + * have access grants while keeping the schema default private for new hosts. + * + * Re-syncing also repairs snapshots for hosts already marked as shared. If the + * privacy migration has previously completed, false values are treated as an + * explicit choice and are never changed back to shared. + */ +export async function runLegacySharedSshAuthOptInMigration(): Promise { + const settingsRepository = createCurrentSettingsRepository(); + + try { + if ((await settingsRepository.get(MIGRATION_FLAG)) === "done") { + return null; + } + + // The behavior being preserved here belongs to releases that only ran on + // SQLite, so Postgres and MySQL have no legacy shares to opt in. The probes + // below are synchronous and sqlite_master specific besides. + if (!needsExplicitPersist(resolveDatabaseDialect())) { + return { enabled: 0, resynced: 0, skipped: 0 }; + } + + const sqlite = getCurrentRepositorySqlite(); + const privacyMigrationAlreadyRan = + (await settingsRepository.get("private_shared_ssh_auth_v1")) === "done"; + const hasLegacySharedCredentialsTable = !!sqlite + .prepare( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'shared_credentials'", + ) + .get(); + const now = new Date().toISOString(); + const legacySnapshotEvidence = hasLegacySharedCredentialsTable + ? ` OR EXISTS ( + SELECT 1 + FROM shared_credentials sc + INNER JOIN host_access legacy_ha + ON legacy_ha.id = sc.host_access_id + WHERE legacy_ha.host_id = ssh_data.id + AND ( + legacy_ha.expires_at IS NULL + OR legacy_ha.expires_at >= ? + ) + )` + : ""; + const updateResult = privacyMigrationAlreadyRan + ? { changes: 0 } + : sqlite + .prepare( + `UPDATE ssh_data + SET share_ssh_auth = 1 + WHERE share_ssh_auth = 0 + AND ( + EXISTS ( + SELECT 1 + FROM shared_host_secrets shs + INNER JOIN host_access ha + ON ha.id = shs.host_access_id + WHERE ha.host_id = ssh_data.id + AND shs.protocol = 'ssh' + AND ( + ha.expires_at IS NULL + OR ha.expires_at >= ? + ) + ) + ${legacySnapshotEvidence} + )`, + ) + .run(...(hasLegacySharedCredentialsTable ? [now, now] : [now])); + const sharedHosts = sqlite + .prepare( + `SELECT DISTINCT h.id + FROM ssh_data h + INNER JOIN host_access ha ON ha.host_id = h.id + WHERE h.share_ssh_auth = 1`, + ) + .all() as SharedHostRow[]; + + const result: LegacySharedSshAuthOptInResult = { + enabled: updateResult.changes, + resynced: 0, + skipped: 0, + }; + const secretsManager = SharedHostSecretsManager.getInstance(); + + for (const host of sharedHosts) { + try { + await secretsManager.resyncHost(host.id); + result.resynced++; + } catch (error) { + result.skipped++; + databaseLogger.warn( + "Failed to resync legacy shared SSH authentication", + { + operation: "legacy_shared_ssh_auth_opt_in_resync_skip", + hostId: host.id, + error: getErrorMessage(error), + }, + ); + } + } + + await settingsRepository.set(MIGRATION_FLAG, "done"); + await DatabaseSaveTrigger.forceSave( + "legacy_shared_ssh_auth_opt_in_migration", + ); + + databaseLogger.info("Preserved legacy shared SSH authentication behavior", { + operation: "legacy_shared_ssh_auth_opt_in_migration", + ...result, + }); + + return result; + } catch (error) { + databaseLogger.error( + "Failed to preserve legacy shared SSH authentication behavior", + error, + { + operation: "legacy_shared_ssh_auth_opt_in_migration", + }, + ); + return { enabled: 0, resynced: 0, skipped: 0 }; + } +} diff --git a/src/backend/utils/crypto-migration/private-shared-ssh-auth-migration.ts b/src/backend/utils/crypto-migration/private-shared-ssh-auth-migration.ts new file mode 100644 index 0000000..ce2db65 --- /dev/null +++ b/src/backend/utils/crypto-migration/private-shared-ssh-auth-migration.ts @@ -0,0 +1,69 @@ +import { databaseLogger } from "../logger.js"; +import { DatabaseSaveTrigger } from "../database-save-trigger.js"; +import { + createCurrentSettingsRepository, + getCurrentRepositorySqlite, +} from "../../database/repositories/factory.js"; +import { + needsExplicitPersist, + resolveDatabaseDialect, +} from "../../database/db/dialect.js"; + +const MIGRATION_FLAG = "private_shared_ssh_auth_v1"; + +/** + * Remove SSH snapshots only for hosts whose owners have not enabled SSH auth + * sharing. Legacy shared hosts are opted in before this cleanup runs. + */ +export async function runPrivateSharedSshAuthMigration(): Promise< + number | null +> { + const settingsRepository = createCurrentSettingsRepository(); + + try { + if ((await settingsRepository.get(MIGRATION_FLAG)) === "done") { + return null; + } + + // Only a SQLite deployment can hold these snapshots: they were written by + // releases that predate Postgres and MySQL support. The cleanup also needs a + // synchronous query no other driver has, so this would throw rather than + // find nothing to do. + if (!needsExplicitPersist(resolveDatabaseDialect())) { + return null; + } + + const result = getCurrentRepositorySqlite() + .prepare( + `DELETE FROM shared_host_secrets + WHERE protocol = ? + AND NOT EXISTS ( + SELECT 1 + FROM host_access ha + INNER JOIN ssh_data h ON h.id = ha.host_id + WHERE ha.id = shared_host_secrets.host_access_id + AND h.share_ssh_auth = 1 + )`, + ) + .run("ssh"); + + await settingsRepository.set(MIGRATION_FLAG, "done"); + await DatabaseSaveTrigger.forceSave("private_shared_ssh_auth_migration"); + + databaseLogger.info("Removed legacy shared SSH authentication snapshots", { + operation: "private_shared_ssh_auth_migration", + removed: result.changes, + }); + + return result.changes; + } catch (error) { + databaseLogger.error( + "Failed to remove legacy shared SSH authentication snapshots", + error, + { + operation: "private_shared_ssh_auth_migration", + }, + ); + return 0; + } +} diff --git a/src/backend/utils/crypto-migration/shared-host-secrets-migration.ts b/src/backend/utils/crypto-migration/shared-host-secrets-migration.ts index 963bdbe..0e1b85b 100644 --- a/src/backend/utils/crypto-migration/shared-host-secrets-migration.ts +++ b/src/backend/utils/crypto-migration/shared-host-secrets-migration.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../error-message.js"; import { databaseLogger } from "../logger.js"; import { DataCrypto } from "../data-crypto.js"; import { DatabaseSaveTrigger } from "../database-save-trigger.js"; @@ -6,6 +7,10 @@ import { getCurrentRepositorySqlite, } from "../../database/repositories/factory.js"; import { SharedHostSecretsManager } from "../shared-host-secrets-manager.js"; +import { + needsExplicitPersist, + resolveDatabaseDialect, +} from "../../database/db/dialect.js"; const MIGRATION_FLAG = "shared_host_secrets_migrated_v1"; @@ -35,9 +40,15 @@ export async function runSharedHostSecretsMigration(): Promise<{ return null; } - const sqlite = getCurrentRepositorySqlite(); const result = { snapshotted: 0, skipped: 0 }; + // Same reasoning as legacy-share-cleanup: this rebuilds shares that only a + // pre-existing SQLite deployment can have, using a synchronous query no other + // driver provides. + if (!needsExplicitPersist(resolveDatabaseDialect())) return result; + + const sqlite = getCurrentRepositorySqlite(); + try { const grants = sqlite .prepare( @@ -96,7 +107,7 @@ export async function runSharedHostSecretsMigration(): Promise<{ operation: "shared_host_secrets_migration_failed", hostAccessId: grant.hostAccessId, targetUserId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } diff --git a/src/backend/utils/data-crypto.ts b/src/backend/utils/data-crypto.ts index a92de14..8aa485c 100644 --- a/src/backend/utils/data-crypto.ts +++ b/src/backend/utils/data-crypto.ts @@ -1,5 +1,10 @@ +import { getErrorMessage } from "./error-message.js"; import { FieldCrypto } from "./field-crypto.js"; import { LazyFieldEncryption } from "./lazy-field-encryption.js"; +import { + needsExplicitPersist, + resolveDatabaseDialect, +} from "../database/db/dialect.js"; import { UserKeyManager } from "./user-keys.js"; import { DatabaseSaveTrigger } from "./database-save-trigger.js"; import { databaseLogger } from "./logger.js"; @@ -97,7 +102,7 @@ class DataCrypto { databaseLogger.error("User sensitive fields migration failed", error, { operation: "user_sensitive_migration_failed", userId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); return { migrated: false, migratedTables: [], migratedFieldsCount: 0 }; @@ -203,7 +208,7 @@ class DataCrypto { databaseLogger.error("User sensitive fields migration failed", error, { operation: "user_sensitive_migration_failed", userId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); return { migrated: false, migratedTables: [], migratedFieldsCount: 0 }; @@ -218,6 +223,14 @@ class DataCrypto { migratedTables: string[]; migratedFieldsCount: number; }> { + // Only a database that predates field encryption has plaintext to migrate, + // and only SQLite deployments can predate it โ€” Postgres and MySQL support + // arrived after. The store also needs synchronous queries no other driver + // has, so this would throw rather than find nothing to do. + if (!needsExplicitPersist(resolveDatabaseDialect())) { + return { migrated: false, migratedTables: [], migratedFieldsCount: 0 }; + } + const result = await this.migrateUserSensitiveFieldsInStore( userId, userDataKey, diff --git a/src/backend/utils/data-dir-guard.ts b/src/backend/utils/data-dir-guard.ts new file mode 100644 index 0000000..0fabc6c --- /dev/null +++ b/src/backend/utils/data-dir-guard.ts @@ -0,0 +1,88 @@ +import fs from "fs"; +import path from "path"; +import { DatabaseFileEncryption } from "./database-file-encryption.js"; + +export const ALLOW_EMPTY_DATA_DIR_ENV = "ALLOW_EMPTY_DATA_DIR"; + +/** Thrown when the data directory looks misconfigured rather than empty. */ +export class DataDirMisconfiguredError extends Error { + constructor(message: string) { + super(message); + this.name = "DataDirMisconfiguredError"; + } +} + +/** + * Directories Termix has shipped or documented as a data location. A deployment + * that loses DATA_DIR โ€” an unloaded .env file, an unmounted volume โ€” falls back + * to the default and finds an empty directory, which is indistinguishable from a + * first run. Checking these tells the two apart. + */ +const KNOWN_DATA_DIRS = ["db/data", "data", "/app/data"]; + +const TRUE_VALUES = new Set(["1", "true", "yes", "on"]); + +function hasDatabaseFile(dir: string): boolean { + const dbPath = path.join(dir, "db.sqlite"); + + if (DatabaseFileEncryption.isEncryptedDatabaseFile(`${dbPath}.encrypted`)) { + return true; + } + + try { + return fs.statSync(dbPath).size > 0; + } catch { + return false; + } +} + +/** + * Looks for a database outside the configured data directory. Returns the + * directory holding it, or null when this really is a fresh install. + */ +export function findDatabaseOutsideDataDir( + dataDir: string, + cwd: string = process.cwd(), +): string | null { + const resolvedDataDir = path.resolve(dataDir); + + for (const candidate of KNOWN_DATA_DIRS) { + const dir = path.resolve(cwd, candidate); + if (dir === resolvedDataDir) continue; + if (hasDatabaseFile(dir)) return dir; + } + + return null; +} + +export function isEmptyDataDirAllowed( + env: NodeJS.ProcessEnv = process.env, +): boolean { + return TRUE_VALUES.has( + env[ALLOW_EMPTY_DATA_DIR_ENV]?.trim().toLowerCase() ?? "", + ); +} + +/** + * Refuses to start with a blank database when an existing one sits elsewhere. + * Creating a fresh database in that state looks exactly like data loss: the user + * is asked to register an admin account again while their real data is intact + * one directory over. + */ +export function assertDataDirIsNotMisconfigured( + dataDir: string, + env: NodeJS.ProcessEnv = process.env, + cwd: string = process.cwd(), +): void { + if (isEmptyDataDirAllowed(env)) return; + + const existing = findDatabaseOutsideDataDir(dataDir, cwd); + if (!existing) return; + + throw new DataDirMisconfiguredError( + `No database found in DATA_DIR (${path.resolve(dataDir)}), but an existing database is present in ${existing}. ` + + `Starting here would create an empty database and hide your data. ` + + `Set DATA_DIR=${existing} (check that your .env file is loaded and any volume is mounted), ` + + `or set ${ALLOW_EMPTY_DATA_DIR_ENV}=true to start with a new database anyway.`, + ); +} diff --git a/src/backend/utils/database-file-encryption.ts b/src/backend/utils/database-file-encryption.ts index cb5400e..a8ccc11 100644 --- a/src/backend/utils/database-file-encryption.ts +++ b/src/backend/utils/database-file-encryption.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "./error-message.js"; import crypto from "crypto"; import fs from "fs"; import path from "path"; @@ -113,10 +114,7 @@ class DatabaseFileEncryption { databaseLogger.warn("Failed to cleanup old metadata file", { operation: "old_meta_cleanup_failed", path: metadataPath, - error: - cleanupError instanceof Error - ? cleanupError.message - : "Unknown error", + error: getErrorMessage(cleanupError), }); } @@ -130,10 +128,7 @@ class DatabaseFileEncryption { databaseLogger.warn("Failed to cleanup temporary files", { operation: "temp_file_cleanup_failed", tmpPath, - error: - cleanupError instanceof Error - ? cleanupError.message - : "Unknown error", + error: getErrorMessage(cleanupError), }); } @@ -142,7 +137,7 @@ class DatabaseFileEncryption { targetPath, }); throw new Error( - `Database buffer encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`, + `Database buffer encryption failed: ${getErrorMessage(error)}`, { cause: error }, ); } @@ -229,10 +224,7 @@ class DatabaseFileEncryption { databaseLogger.warn("Failed to cleanup temporary files", { operation: "temp_file_cleanup_failed", tmpPath, - error: - cleanupError instanceof Error - ? cleanupError.message - : "Unknown error", + error: getErrorMessage(cleanupError), }); } @@ -242,7 +234,7 @@ class DatabaseFileEncryption { targetPath: encryptedPath, }); throw new Error( - `Database file encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`, + `Database file encryption failed: ${getErrorMessage(error)}`, { cause: error }, ); } @@ -357,8 +349,7 @@ class DatabaseFileEncryption { return decryptedBuffer; } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; + const errorMessage = getErrorMessage(error); const isAuthError = errorMessage.includes("Unsupported state") || errorMessage.includes("authenticate data") || @@ -449,7 +440,7 @@ class DatabaseFileEncryption { targetPath: decryptedPath, }); throw new Error( - `Database file decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`, + `Database file decryption failed: ${getErrorMessage(error)}`, { cause: error }, ); } @@ -748,7 +739,7 @@ class DatabaseFileEncryption { databaseLogger.warn("Failed to clean up temporary files", { operation: "temp_cleanup_failed", basePath, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } diff --git a/src/backend/utils/database-migration.ts b/src/backend/utils/database-migration.ts index 3ddcb86..bf676e8 100644 --- a/src/backend/utils/database-migration.ts +++ b/src/backend/utils/database-migration.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "./error-message.js"; import fs from "fs"; import path from "path"; import { databaseLogger } from "./logger.js"; @@ -45,7 +46,7 @@ export class DatabaseMigration { } catch (error) { databaseLogger.warn("Could not get unencrypted database file size", { operation: "migration_status_check", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } @@ -65,7 +66,7 @@ export class DatabaseMigration { } catch (error) { databaseLogger.warn("Failed to remove empty unencrypted database", { operation: "migration_cleanup_empty_failed", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } else { @@ -117,10 +118,9 @@ export class DatabaseMigration { source: this.unencryptedDbPath, backup: backupPath, }); - throw new Error( - `Backup creation failed: ${error instanceof Error ? error.message : "Unknown error"}`, - { cause: error }, - ); + throw new Error(`Backup creation failed: ${getErrorMessage(error)}`, { + cause: error, + }); } } @@ -174,8 +174,7 @@ export class DatabaseMigration { duration: Date.now() - startTime, }; } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; + const errorMessage = getErrorMessage(error); databaseLogger.error("Database migration failed", error, { operation: "migration_failed", @@ -233,14 +232,14 @@ export class DatabaseMigration { databaseLogger.warn("Failed to cleanup old migration file", { operation: "migration_cleanup_failed", file: file.name, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } } catch (error) { databaseLogger.warn("Migration cleanup failed", { operation: "migration_cleanup_error", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); } } diff --git a/src/backend/utils/database-save-trigger.ts b/src/backend/utils/database-save-trigger.ts index 08d07e4..583286d 100644 --- a/src/backend/utils/database-save-trigger.ts +++ b/src/backend/utils/database-save-trigger.ts @@ -1,9 +1,11 @@ +import { getErrorMessage } from "./error-message.js"; import { databaseLogger } from "./logger.js"; export class DatabaseSaveTrigger { private static saveFunction: (() => Promise) | null = null; private static isInitialized = false; private static pendingSave = false; + private static activeSave: Promise | null = null; private static saveTimeout: NodeJS.Timeout | null = null; private static _dirty = false; @@ -38,23 +40,17 @@ export class DatabaseSaveTrigger { } this.saveTimeout = setTimeout(async () => { - if (this.pendingSave) { - return; - } - - this.pendingSave = true; + this.saveTimeout = null; try { - await this.saveFunction!(); + await this.runSave(); this._dirty = false; } catch (error) { databaseLogger.error("Database save failed", error, { operation: "db_save_trigger_failed", reason, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); - } finally { - this.pendingSave = false; } }, 2000); } @@ -76,23 +72,39 @@ export class DatabaseSaveTrigger { this.saveTimeout = null; } - if (this.pendingSave) { - return; - } - - this.pendingSave = true; - try { - await this.saveFunction(); + await this.runSave(); + this._dirty = false; } catch (error) { databaseLogger.error("Database force save failed", error, { operation: "db_save_trigger_force_failed", reason, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); throw error; + } + } + + private static async runSave(): Promise { + while (this.activeSave) { + try { + await this.activeSave; + } catch { + // The queued save must still run after an earlier save failed. + } + } + + const save = Promise.resolve().then(() => this.saveFunction!()); + this.activeSave = save; + this.pendingSave = true; + + try { + await save; } finally { - this.pendingSave = false; + if (this.activeSave === save) { + this.activeSave = null; + this.pendingSave = false; + } } } @@ -115,6 +127,7 @@ export class DatabaseSaveTrigger { } this.pendingSave = false; + this.activeSave = null; this.isInitialized = false; this.saveFunction = null; } diff --git a/src/backend/utils/discord-sender.ts b/src/backend/utils/discord-sender.ts new file mode 100644 index 0000000..817ff5b --- /dev/null +++ b/src/backend/utils/discord-sender.ts @@ -0,0 +1,90 @@ +import { safeOutboundFetch } from "./safe-outbound-fetch.js"; +import { statsLogger } from "./logger.js"; + +export interface DiscordConfig { + url: string; + username?: string; + avatar_url?: string; +} + +import type { AlertPayload } from "./notification-sender.js"; + +async function fetchWithRetry( + url: string, + options: RequestInit, +): Promise { + const attempt = async () => { + const res = await safeOutboundFetch(url, options); + if (!res.ok) { + let body = ""; + try { + body = await res.text(); + } catch { + /* ignore */ + } + throw new Error( + `HTTP ${res.status}: ${res.statusText}${body ? ` - ${body}` : ""}`, + ); + } + }; + + try { + await attempt(); + } catch (firstErr) { + await new Promise((r) => setTimeout(r, 3000)); + try { + await attempt(); + } catch (secondErr) { + statsLogger.warn("Discord notification delivery failed after retry", { + operation: "discord_notification_send_failed", + url, + error: + secondErr instanceof Error ? secondErr.message : String(secondErr), + }); + throw secondErr; + } + } +} + +export async function sendDiscord( + config: DiscordConfig, + payload: AlertPayload, +): Promise { + const { url, username, avatar_url } = config; + const colorMap: Record = { + info: 3066993, + warning: 16753920, + critical: 15158332, + }; + const color = colorMap[payload.severity] ?? 3447003; + + const embed: Record = { + title: `[Termix] ${payload.hostName}: ${payload.ruleName}`, + description: payload.message, + color, + fields: [ + { name: "Host", value: payload.hostName || "โ€”", inline: true }, + { name: "Rule", value: payload.ruleName || "โ€”", inline: true }, + { name: "Severity", value: payload.severity, inline: true }, + ], + timestamp: payload.timestamp, + } as Record; + + if (payload.value !== undefined && payload.value !== null) { + (embed.fields as any).push({ + name: "Value", + value: String(payload.value), + inline: true, + }); + } + + const body: Record = { embeds: [embed] }; + if (username) (body as any).username = username; + if (avatar_url) (body as any).avatar_url = avatar_url; + + await fetchWithRetry(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} diff --git a/src/backend/utils/error-message.ts b/src/backend/utils/error-message.ts new file mode 100644 index 0000000..ad7d83b --- /dev/null +++ b/src/backend/utils/error-message.ts @@ -0,0 +1,14 @@ +/** + * Extract a stable, human-readable message from an unknown thrown value. + * + * A thrown value is not guaranteed to be an `Error` โ€” libraries may throw + * strings or plain objects, which carry no useful `.message`. When that + * happens the caller gets `fallback` instead, so error paths never surface a + * raw `[object Object]` in logs or responses. + */ +export function getErrorMessage( + error: unknown, + fallback = "Unknown error", +): string { + return error instanceof Error ? error.message : fallback; +} diff --git a/src/backend/utils/field-crypto.ts b/src/backend/utils/field-crypto.ts index a5352b6..1528aea 100644 --- a/src/backend/utils/field-crypto.ts +++ b/src/backend/utils/field-crypto.ts @@ -42,9 +42,14 @@ class FieldCrypto { "key", "publicKey", ]), + // Channel configs hold ntfy tokens, webhook auth headers and Discord + // webhook URLs, which are credentials like any other. + notification_channels: new Set(["config"]), opkssh_tokens: new Set(["sshCert", "privateKey"]), termix_identity_ca: new Set(["privateKey"]), vault_tokens: new Set(["sshCert", "privateKey"]), + // Third-party AI provider keys are user credentials like any other. + ai_providers: new Set(["apiKey"]), }; static encryptField( @@ -83,6 +88,26 @@ class FieldCrypto { return JSON.stringify(encryptedData); } + /** + * Whether a stored value is one of our envelopes rather than plaintext. + * Some encrypted fields hold JSON themselves, so parsing is not enough; the + * envelope's own keys have to be present. + */ + static isEncrypted(value: string): boolean { + if (!value || !value.startsWith("{")) return false; + try { + const parsed = JSON.parse(value) as Partial; + return ( + typeof parsed?.data === "string" && + typeof parsed?.iv === "string" && + typeof parsed?.tag === "string" && + typeof parsed?.salt === "string" + ); + } catch { + return false; + } + } + static decryptField( encryptedValue: string, masterKey: Buffer, diff --git a/src/backend/utils/lazy-field-encryption.ts b/src/backend/utils/lazy-field-encryption.ts index 0c3d0a3..19b2bec 100644 --- a/src/backend/utils/lazy-field-encryption.ts +++ b/src/backend/utils/lazy-field-encryption.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "./error-message.js"; import { FieldCrypto } from "./field-crypto.js"; import { databaseLogger } from "./logger.js"; import type { UserEncryptionMigrationStore } from "./user-encryption-migration-store.js"; @@ -145,7 +146,7 @@ export class LazyFieldEncryption { operation: "lazy_encryption_decrypt_failed", recordId, fieldName, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); throw error; } @@ -181,7 +182,7 @@ export class LazyFieldEncryption { operation: "lazy_encryption_migrate_failed", recordId, fieldName, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); throw error; } @@ -470,7 +471,7 @@ export class LazyFieldEncryption { databaseLogger.error("Failed to check user migration needs", error, { operation: "lazy_encryption_user_check_failed", userId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); return { needsMigration: false, plaintextFields: [] }; diff --git a/src/backend/utils/logger.ts b/src/backend/utils/logger.ts index 73b28f6..3ce1a83 100644 --- a/src/backend/utils/logger.ts +++ b/src/backend/utils/logger.ts @@ -1,5 +1,4 @@ -import chalk from "chalk"; -import type { ChalkInstance } from "chalk"; +import chalk, { type ChalkInstance } from "chalk"; export type LogLevel = "debug" | "info" | "warn" | "error" | "success"; @@ -149,8 +148,24 @@ export class Logger { contextParts.push(`session:${sanitizedContext.sessionId}`); if (sanitizedContext.requestId) contextParts.push(`req:${sanitizedContext.requestId}`); + if (sanitizedContext.source) + contextParts.push(`source:${sanitizedContext.source}`); + if (sanitizedContext.sequence !== undefined) + contextParts.push(`seq:${sanitizedContext.sequence}`); + if (sanitizedContext.clientUploadTimestamp) + contextParts.push( + `clientUpload:${sanitizedContext.clientUploadTimestamp}`, + ); + if (sanitizedContext.serverReceivedAt) + contextParts.push( + `serverReceived:${sanitizedContext.serverReceivedAt}`, + ); + if (sanitizedContext.bytes !== undefined) + contextParts.push(`bytes:${sanitizedContext.bytes}`); if (sanitizedContext.duration) contextParts.push(`duration:${sanitizedContext.duration}ms`); + if (sanitizedContext.error) + contextParts.push(`error:${sanitizedContext.error}`); if (contextParts.length > 0) { contextStr = chalk.gray(` [${contextParts.join(",")}]`); diff --git a/src/backend/utils/nginx-ssl-reload.ts b/src/backend/utils/nginx-ssl-reload.ts new file mode 100644 index 0000000..ee72c21 --- /dev/null +++ b/src/backend/utils/nginx-ssl-reload.ts @@ -0,0 +1,106 @@ +import { execFileSync } from "child_process"; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import path from "path"; +import { authLogger } from "./logger.js"; + +const NGINX_TEMPLATE = "/app/nginx/nginx-https.conf.template"; +const NGINX_CONF = "/tmp/nginx/nginx.conf"; +const NGINX_PID = "/tmp/nginx/nginx.pid"; + +const DATA_DIR = process.env.DATA_DIR || "./db/data"; +const SSL_DIR = path.join(DATA_DIR, "ssl"); +const ENV_FILE = path.join(DATA_DIR, ".env"); + +function persistSSLEnv(sslPort: string, certPath: string, keyPath: string) { + const vars: Record = { + ENABLE_SSL: "true", + SSL_PORT: sslPort, + SSL_CERT_PATH: certPath, + SSL_KEY_PATH: keyPath, + }; + + let content = ""; + try { + content = readFileSync(ENV_FILE, "utf8"); + } catch { + // no existing .env file yet + } + + for (const [key, value] of Object.entries(vars)) { + const regex = new RegExp(`^${key}=.*$`, "m"); + if (regex.test(content)) { + content = content.replace(regex, `${key}=${value}`); + } else { + if (!content.includes("# SSL Configuration")) { + content += `\n# SSL Configuration (Auto-generated)\n`; + } + content += `${key}=${value}\n`; + } + } + + writeFileSync(ENV_FILE, content.trim() + "\n"); +} + +/** + * Regenerates the nginx config from the HTTPS template and reloads nginx so a + * newly issued/uploaded certificate is served without a full container + * restart. No-ops outside the Docker image (template/binary won't exist). + */ +export function reloadNginxWithSSL(): { applied: boolean; message: string } { + if (!existsSync(NGINX_TEMPLATE) || !existsSync(NGINX_PID)) { + return { + applied: false, + message: + "nginx is not managed by this environment; restart Termix with ENABLE_SSL=true to apply the certificate.", + }; + } + + const port = process.env.PORT || "8080"; + const sslPort = process.env.SSL_PORT || "8443"; + const certPath = + process.env.SSL_CERT_PATH || path.join(SSL_DIR, "termix.crt"); + const keyPath = process.env.SSL_KEY_PATH || path.join(SSL_DIR, "termix.key"); + + try { + const template = readFileSync(NGINX_TEMPLATE, "utf8"); + const rendered = template + .split("${PORT}") + .join(port) + .split("${SSL_PORT}") + .join(sslPort) + .split("${SSL_CERT_PATH}") + .join(certPath) + .split("${SSL_KEY_PATH}") + .join(keyPath); + + writeFileSync(NGINX_CONF, rendered); + + execFileSync("nginx", ["-t", "-c", NGINX_CONF], { stdio: "pipe" }); + execFileSync("nginx", ["-s", "reload", "-c", NGINX_CONF], { + stdio: "pipe", + }); + + process.env.ENABLE_SSL = "true"; + process.env.SSL_PORT = sslPort; + process.env.SSL_CERT_PATH = certPath; + process.env.SSL_KEY_PATH = keyPath; + persistSSLEnv(sslPort, certPath, keyPath); + + authLogger.info("nginx reloaded with HTTPS enabled", { + operation: "nginx_ssl_reload", + sslPort, + }); + + return { + applied: true, + message: `HTTPS is now active on port ${sslPort}. Make sure that port is published/mapped to this container.`, + }; + } catch (err) { + authLogger.error("Failed to reload nginx with SSL config", err); + return { + applied: false, + message: + "Certificate installed, but nginx could not be reloaded automatically. Restart Termix with ENABLE_SSL=true to apply it.", + }; + } +} diff --git a/src/backend/utils/opkssh-binary-manager.ts b/src/backend/utils/opkssh-binary-manager.ts index a343819..30d7240 100644 --- a/src/backend/utils/opkssh-binary-manager.ts +++ b/src/backend/utils/opkssh-binary-manager.ts @@ -1,6 +1,6 @@ -import { promises as fs } from "fs"; +import { getErrorMessage } from "./error-message.js"; +import { createWriteStream, promises as fs } from "fs"; import path from "path"; -import { createWriteStream } from "fs"; import { pipeline } from "stream/promises"; import { systemLogger } from "./logger.js"; @@ -16,6 +16,12 @@ function getVersionFile(): string { return path.join(getBinaryDir(), "version.txt"); } +function getBundledDir(): string { + return ( + process.env.OPKSSH_BUNDLED_DIR || path.join(process.cwd(), "opkssh-bundled") + ); +} + interface GitHubAsset { name: string; browser_download_url: string; @@ -50,6 +56,12 @@ export class OPKSSHBinaryManager { this.binaryPath = expectedPath; return expectedPath; } catch { + const usedBundled = await this.useBundledBinary(expectedPath); + if (usedBundled) { + this.binaryPath = expectedPath; + return expectedPath; + } + systemLogger.info("OPKSSH binary not found, downloading...", { operation: "opkssh_binary_download_start", }); @@ -59,6 +71,38 @@ export class OPKSSHBinaryManager { } } + private static async useBundledBinary( + expectedPath: string, + ): Promise { + const binaryName = this.getBinaryName(); + const bundledPath = path.join(getBundledDir(), binaryName); + const bundledVersionFile = path.join(getBundledDir(), "version.txt"); + + try { + await fs.access(bundledPath); + await fs.mkdir(getBinaryDir(), { recursive: true }); + await fs.copyFile(bundledPath, expectedPath); + await fs.chmod(expectedPath, 0o755); + + try { + const bundledVersion = ( + await fs.readFile(bundledVersionFile, "utf8") + ).trim(); + await fs.writeFile(getVersionFile(), bundledVersion, "utf8"); + } catch { + // Bundled version file is optional + } + + systemLogger.info("Using bundled OPKSSH binary", { + operation: "opkssh_binary_bundled_used", + path: expectedPath, + }); + return true; + } catch { + return false; + } + } + static async downloadBinary(): Promise { try { await fs.mkdir(getBinaryDir(), { recursive: true }); @@ -140,7 +184,7 @@ export class OPKSSHBinaryManager { } catch (error) { systemLogger.warn("Failed to check for OPKSSH updates", { operation: "opkssh_update_check_failed", - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }); return false; } diff --git a/src/backend/utils/permission-catalog.ts b/src/backend/utils/permission-catalog.ts index 6e78af0..8fa7774 100644 --- a/src/backend/utils/permission-catalog.ts +++ b/src/backend/utils/permission-catalog.ts @@ -27,6 +27,16 @@ export const PERMISSION_CATALOG: PermissionCatalogEntry[] = [ "snippets.share", ], }, + { + group: "automations", + permissions: [ + "automations.view", + "automations.create", + "automations.edit", + "automations.delete", + "automations.run", + ], + }, { group: "credentials", permissions: [ @@ -36,6 +46,10 @@ export const PERMISSION_CATALOG: PermissionCatalogEntry[] = [ "credentials.delete", ], }, + { + group: "ai", + permissions: ["ai.use", "ai.manage_providers", "ai.apply_proposals"], + }, { group: "admin", permissions: [ diff --git a/src/backend/utils/permission-manager.ts b/src/backend/utils/permission-manager.ts index a312072..11140bd 100644 --- a/src/backend/utils/permission-manager.ts +++ b/src/backend/utils/permission-manager.ts @@ -16,7 +16,7 @@ const SHARE_PERMISSION_LEVELS = ["connect", "view", "edit", "manage"] as const; type SharePermissionLevel = (typeof SHARE_PERMISSION_LEVELS)[number]; -type HostAction = SharePermissionLevel | "delete"; +export type HostAction = SharePermissionLevel | "delete"; const LEVEL_RANK: Record = { connect: 1, @@ -70,8 +70,12 @@ class PermissionManager { }); }, 60 * 1000); + // Entries expire on read against their own timestamp, so this sweep only + // has to drop ones nobody has come back for. Flushing the whole map on a + // timer instead expired every active user at the same instant, so each + // sweep was followed by a burst of simultaneous role lookups. setInterval(() => { - this.clearPermissionCache(); + this.evictExpiredPermissions(); }, this.CACHE_TTL); } @@ -92,8 +96,13 @@ class PermissionManager { } } - private clearPermissionCache(): void { - this.permissionCache.clear(); + private evictExpiredPermissions(): void { + const now = Date.now(); + for (const [userId, entry] of this.permissionCache) { + if (now - entry.timestamp >= this.CACHE_TTL) { + this.permissionCache.delete(userId); + } + } } invalidateUserPermissionCache(userId: string): void { @@ -112,10 +121,22 @@ class PermissionManager { const allPermissions = new Set(); for (const record of userRoleRecords) { + // A role can legitimately have no permissions column yet, and + // JSON.parse(null) returns null rather than throwing, which used to + // blow up the loop and leave the user with no permissions at all. + if (!record.permissions) continue; + try { - const permissions = JSON.parse(record.permissions) as string[]; - for (const perm of permissions) { - allPermissions.add(perm); + const parsed = JSON.parse(record.permissions) as unknown; + if (!Array.isArray(parsed)) { + databaseLogger.warn("Role permissions are not a list", { + operation: "get_user_permissions", + userId, + }); + continue; + } + for (const perm of parsed) { + if (typeof perm === "string") allPermissions.add(perm); } } catch (parseError) { databaseLogger.warn("Failed to parse role permissions", { @@ -268,6 +289,55 @@ class PermissionManager { } } + /** + * The subset of `hostIds` this user may reach, resolved in a fixed number of + * queries instead of one call per host. + * + * canAccessHost costs between one and four queries, so filtering a list with + * it is linear in host count โ€” and the status poll does exactly that every + * few seconds for the whole fleet. This answers the same question for many + * hosts at once using the same three rules, in the same order: owner, then + * an unexpired grant, then admin bypass. + * + * Deliberately limited to read-style checks. It does not touch grant + * timestamps the way `canAccessHost(..., "connect")` does, because this is + * used for visibility filtering rather than for opening a connection. + */ + async filterAccessibleHostIds( + userId: string, + hostIds: number[], + ): Promise> { + if (hostIds.length === 0) return new Set(); + + try { + if (await this.isAdmin(userId)) { + return new Set(hostIds); + } + + const owned = + await createCurrentHostResolutionRepository().listOwnedHostIds(userId); + + const roleIds = + await createCurrentRoleRepository().listUserRoleIds(userId); + const grants = + await createCurrentRbacAccessRepository().listVisibleHostAccessEntries( + userId, + roleIds, + ); + const granted = new Set(grants.map((grant) => grant.hostId)); + + return new Set(hostIds.filter((id) => owned.has(id) || granted.has(id))); + } catch (error) { + databaseLogger.error("Failed to filter accessible hosts", error, { + operation: "filter_accessible_hosts", + userId, + }); + // Fail closed: showing nothing is safer than showing another + // tenant's hosts. + return new Set(); + } + } + // Admins get owner-equivalent access to every host; each connect is // audit-logged in the host resolver. private adminBypassAccess(): HostAccessInfo { @@ -416,5 +486,4 @@ export type { HostAccessInfo, PermissionCheckResult, SharePermissionLevel, - HostAction, }; diff --git a/src/backend/utils/proxy-agent.ts b/src/backend/utils/proxy-agent.ts index 420c975..ec9b392 100644 --- a/src/backend/utils/proxy-agent.ts +++ b/src/backend/utils/proxy-agent.ts @@ -1,6 +1,13 @@ -import { ProxyAgent } from "undici"; +import { Agent, ProxyAgent } from "undici"; import type { Dispatcher } from "undici-types"; +const directAgent = new Agent({ + connect: { + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout: 250, + }, +}); + export function getProxyAgent(targetUrl?: string): Dispatcher | undefined { const proxyUrl = process.env.https_proxy || @@ -28,3 +35,7 @@ export function getProxyAgent(targetUrl?: string): Dispatcher | undefined { return new ProxyAgent(proxyUrl) as unknown as Dispatcher; } + +export function getFetchDispatcher(targetUrl: string): Dispatcher { + return getProxyAgent(targetUrl) ?? (directAgent as unknown as Dispatcher); +} diff --git a/src/backend/utils/proxy-helper.ts b/src/backend/utils/proxy-helper.ts index a2d1795..4a8292a 100644 --- a/src/backend/utils/proxy-helper.ts +++ b/src/backend/utils/proxy-helper.ts @@ -1,25 +1,11 @@ -import { SocksClient } from "socks"; -import type { SocksClientOptions } from "socks"; +import { getErrorMessage } from "./error-message.js"; +import { SocksClient, type SocksClientOptions } from "socks"; import net from "net"; import dns from "dns/promises"; import { sshLogger } from "./logger.js"; +import { isBlockedAddress } from "./safe-outbound-fetch.js"; import type { ProxyNode } from "../../types/index.js"; -function isBlockedAddress(ip: string): boolean { - if (ip === "0.0.0.0" || ip === "::1" || ip === "::") return true; - - const parts = ip.split(".").map(Number); - if (parts.length !== 4) return false; - - if (parts[0] === 127) return true; - if (parts[0] === 10) return true; - if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; - if (parts[0] === 192 && parts[1] === 168) return true; - if (parts[0] === 169 && parts[1] === 254) return true; - - return false; -} - async function validateHost(host: string): Promise { if (net.isIP(host)) { if (isBlockedAddress(host)) { @@ -43,7 +29,7 @@ export interface SOCKS5Config { socks5ProxyChain?: ProxyNode[]; } -export async function createProxyConnection( +export async function createSocks5Connection( targetHost: string, targetPort: number, socks5Config: SOCKS5Config, @@ -70,8 +56,6 @@ export async function createProxyConnection( return null; } -export const createSocks5Connection = createProxyConnection; - async function createSingleProxyConnection( targetHost: string, targetPort: number, @@ -103,7 +87,7 @@ async function createSingleProxyConnection( proxyPort: socks5Config.socks5Port || 1080, targetHost, targetPort, - errorMessage: error instanceof Error ? error.message : "Unknown error", + errorMessage: getErrorMessage(error), }); throw error; } @@ -237,7 +221,7 @@ async function createPureSocksChainConnection( chainLength: proxyChain.length, targetHost, targetPort, - errorMessage: error instanceof Error ? error.message : "Unknown error", + errorMessage: getErrorMessage(error), }); throw error; } @@ -301,7 +285,7 @@ async function createHopByHopConnection( chainLength: proxyChain.length, targetHost, targetPort, - errorMessage: error instanceof Error ? error.message : "Unknown error", + errorMessage: getErrorMessage(error), }); throw error; } diff --git a/src/backend/utils/request-origin.ts b/src/backend/utils/request-origin.ts index 7ecf54e..71d457d 100644 --- a/src/backend/utils/request-origin.ts +++ b/src/backend/utils/request-origin.ts @@ -64,6 +64,20 @@ export function normalizeBasePath(value: unknown): string { return basePath.replace(/\/+$/, ""); } +/** + * Real client IP behind a reverse proxy. `X-Forwarded-For`'s leftmost entry is + * the original client; socket.remoteAddress is only the immediate peer, which + * behind Traefik/Cloudflare is the proxy itself (often a loopback address). + */ +export function getClientIp(req: Request | IncomingMessage): string { + const forwarded = firstHeaderValue(req.headers["x-forwarded-for"]); + if (forwarded) return forwarded; + + if ("ip" in req && req.ip) return req.ip; + + return req.socket?.remoteAddress ?? "unknown"; +} + export function getRequestOrigin(req: Request | IncomingMessage): string { let protocol: string; const protoHeader = req.headers["x-forwarded-proto"]; diff --git a/src/backend/utils/safe-outbound-fetch.ts b/src/backend/utils/safe-outbound-fetch.ts index 1448be0..5eaf78a 100644 --- a/src/backend/utils/safe-outbound-fetch.ts +++ b/src/backend/utils/safe-outbound-fetch.ts @@ -1,28 +1,57 @@ -import { lookup } from "dns"; +import { lookup, type LookupAddress, type LookupOptions } from "dns"; import { BlockList, isIP } from "net"; -import { Agent } from "undici"; +import { Agent, fetch as undiciFetch } from "undici"; + +type DnsLookupFn = ( + hostname: string, + options: LookupOptions, + callback: DnsLookupCallback, +) => void; + +type DnsLookupCallback = ( + err: NodeJS.ErrnoException | null, + address: string | LookupAddress[] | undefined, + family?: number, +) => void; + +type LookupHookCallback = ( + error: NodeJS.ErrnoException | Error | null, + address?: string | LookupAddress[], + family?: number, +) => void; const blockedAddresses = new BlockList(); -for (const [network, prefix] of [ +// Derived, not hand-duplicated: Node's BlockList matches addresses across +// families through their IPv4-mapped-IPv6 form regardless of which `type` +// you pass to check()/addSubnet() (see the addAddress('123.123.123.123') / +// check('::ffff:123.123.123.123') example on +// https://nodejs.org/api/net.html#class-netblocklist). So every IPv4 range +// below needs an "::ffff:" mirror in the IPv6 list, or a spoofed +// literal like "::ffff:127.0.0.1" slips through unblocked. Generating the +// mirror from this list instead of maintaining two lists by hand means the +// two can't drift out of sync the way they did before. +const blockedIpv4Ranges = [ ["0.0.0.0", 8], ["10.0.0.0", 8], - ["100.64.0.0", 10], + ["100.64.0.0", 10], // CGNAT ["127.0.0.0", 8], - ["169.254.0.0", 16], + ["169.254.0.0", 16], // link-local ["172.16.0.0", 12], ["192.168.0.0", 16], - ["198.18.0.0", 15], - ["224.0.0.0", 4], - ["240.0.0.0", 4], -] as const) { + ["198.18.0.0", 15], // benchmarking + ["224.0.0.0", 4], // multicast + ["240.0.0.0", 4], // reserved +] as const; + +for (const [network, prefix] of blockedIpv4Ranges) { blockedAddresses.addSubnet(network, prefix, "ipv4"); + blockedAddresses.addSubnet(`::ffff:${network}`, prefix + 96, "ipv6"); } for (const [network, prefix] of [ ["::", 128], ["::1", 128], - ["::ffff:0:0", 96], ["fc00::", 7], ["fe80::", 10], ["ff00::", 8], @@ -30,7 +59,7 @@ for (const [network, prefix] of [ blockedAddresses.addSubnet(network, prefix, "ipv6"); } -function isBlockedAddress(address: string): boolean { +export function isBlockedAddress(address: string): boolean { const family = isIP(address); return ( family === 0 || @@ -38,6 +67,80 @@ function isBlockedAddress(address: string): boolean { ); } +// Extracted so the blocklist decision can be tested directly against a +// fake DNS resolver, instead of only through a real fetch()/Agent call โ€” +// the actual bug here lived entirely in this callback, several layers +// below where undici's own "fetch failed" wrapping would otherwise hide it. +export function createDnsLookupHook(dnsLookup: DnsLookupFn = lookup) { + return function lookupHook( + host: string, + lookupOptions: LookupOptions, + callback: LookupHookCallback, + ): void { + const cleanHost = String(host ?? "").replace(/^[|]$/g, ""); + const lookupAll = lookupOptions.all === true; + + dnsLookup( + cleanHost, + { ...lookupOptions, all: true, verbatim: true }, + (error, addresses, family) => { + if (error) { + return callback(error, "", 0); + } + + const addrs = Array.isArray(addresses) + ? addresses + : addresses != null + ? [{ address: addresses, family: family ?? isIP(addresses) }] + : undefined; + + if (addrs === undefined) { + return callback( + new Error("DNS lookup returned invalid address"), + "", + 0, + ); + } + + if (!addrs.length) { + return callback( + new Error("DNS resolution returned no addresses"), + "", + 0, + ); + } + + if (addrs.some(({ address }) => isBlockedAddress(address))) { + return callback( + new Error("Private destinations are not allowed"), + "", + 0, + ); + } + + if (lookupAll) { + return callback(null, addrs, 0); + } + + const result = addrs[0]; + const addr = String(result.address ?? "").replace(/^\[|\]$/g, ""); + const fam = + typeof result.family === "number" ? result.family : isIP(addr); + + if (!addr || isIP(addr) === 0) { + return callback( + new Error("DNS lookup returned invalid address"), + "", + 0, + ); + } + + return callback(null, addr, fam); + }, + ); + }; +} + export async function safeOutboundFetch( rawUrl: string, options: RequestInit, @@ -58,36 +161,16 @@ export async function safeOutboundFetch( const dispatcher = new Agent({ connect: { - lookup(host, lookupOptions, callback) { - lookup( - host, - { ...lookupOptions, all: true, verbatim: true }, - (error, addresses) => { - if (error) return callback(error, "", 0); - if ( - !addresses.length || - addresses.some(({ address }) => isBlockedAddress(address)) - ) { - return callback( - new Error("Private destinations are not allowed"), - "", - 0, - ); - } - const selected = addresses[0]; - callback(null, selected.address, selected.family); - }, - ); - }, + lookup: createDnsLookupHook(lookup), }, }); try { - return await fetch(url, { + return await undiciFetch(url.toString(), { ...options, - redirect: "error", dispatcher, - } as RequestInit & { dispatcher: Agent }); + redirect: "error", + }); } finally { await dispatcher.close(); } diff --git a/src/backend/utils/shared-host-auth-override-migration.ts b/src/backend/utils/shared-host-auth-override-migration.ts new file mode 100644 index 0000000..5f8be1e --- /dev/null +++ b/src/backend/utils/shared-host-auth-override-migration.ts @@ -0,0 +1,101 @@ +import type Database from "better-sqlite3"; + +const MIGRATION_KEY = "shared_host_auth_overrides_v1"; + +const createProtocolAwareTableSql = ` + CREATE TABLE shared_host_auth_overrides ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT NOT NULL, + protocol TEXT NOT NULL DEFAULT 'ssh', + credential_id INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (credential_id) REFERENCES ssh_credentials (id) ON DELETE CASCADE + ); + CREATE UNIQUE INDEX shared_host_auth_overrides_host_user_protocol_unique + ON shared_host_auth_overrides (host_id, user_id, protocol); +`; + +export type SharedHostAuthOverrideSchemaResult = + "created" | "migrated" | "current"; + +/** + * Keeps the override storage protocol-capable without enabling any additional + * protocol. Pre-protocol rows are preserved as SSH overrides. + */ +export function ensureSharedHostAuthOverrideProtocolSchema( + sqlite: Database.Database, +): SharedHostAuthOverrideSchemaResult { + const tableExists = sqlite + .prepare( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'shared_host_auth_overrides'", + ) + .get(); + + if (!tableExists) { + sqlite.exec(createProtocolAwareTableSql); + return "created"; + } + + const hasProtocol = sqlite + .prepare( + "SELECT 1 FROM pragma_table_info('shared_host_auth_overrides') WHERE name = 'protocol'", + ) + .get(); + if (hasProtocol) { + return "current"; + } + + sqlite.transaction(() => { + sqlite.exec(` + ALTER TABLE shared_host_auth_overrides + RENAME TO shared_host_auth_overrides_pre_protocol; + + ${createProtocolAwareTableSql} + + INSERT INTO shared_host_auth_overrides + (id, host_id, user_id, protocol, credential_id, created_at, updated_at) + SELECT + id, host_id, user_id, 'ssh', credential_id, created_at, updated_at + FROM shared_host_auth_overrides_pre_protocol; + + DROP TABLE shared_host_auth_overrides_pre_protocol; + `); + })(); + + return "migrated"; +} + +export function migrateLegacySharedHostAuthOverrides( + sqlite: Database.Database, + getSetting: (key: string) => string | null, + setSetting: (key: string, value: string) => void, +): boolean { + if (getSetting(MIGRATION_KEY) !== null) return false; + + const hasLegacyColumn = sqlite + .prepare( + "SELECT 1 FROM pragma_table_info('host_access') WHERE name = 'override_credential_id'", + ) + .get(); + + if (hasLegacyColumn) { + sqlite.exec(` + INSERT OR IGNORE INTO shared_host_auth_overrides + (host_id, user_id, protocol, credential_id) + SELECT host_id, user_id, 'ssh', override_credential_id + FROM host_access + WHERE user_id IS NOT NULL AND override_credential_id IS NOT NULL; + + UPDATE host_access + SET override_credential_id = NULL + WHERE override_credential_id IS NOT NULL; + `); + } + + setSetting(MIGRATION_KEY, "done"); + return true; +} diff --git a/src/backend/utils/shared-host-auth-override-service.ts b/src/backend/utils/shared-host-auth-override-service.ts new file mode 100644 index 0000000..532685f --- /dev/null +++ b/src/backend/utils/shared-host-auth-override-service.ts @@ -0,0 +1,136 @@ +import { + AUTH_PROTOCOL_METADATA, + isSupportedAuthOverrideProtocol, + type AuthOverrideProtocol, +} from "../../types/auth-protocols.js"; +import { + createCurrentCredentialRepository, + createCurrentSharedHostAuthOverrideRepository, + createCurrentUserRepository, +} from "../database/repositories/factory.js"; +import { logAudit } from "./audit-logger.js"; +import { PermissionManager } from "./permission-manager.js"; + +export interface SharedHostAuthOverrideAuditContext { + ipAddress?: string; + userAgent?: string; +} + +export class SharedHostAuthOverrideServiceError extends Error { + constructor( + message: string, + readonly statusCode: number, + ) { + super(message); + this.name = "SharedHostAuthOverrideServiceError"; + } +} + +export class SharedHostAuthOverrideService { + private static instance: SharedHostAuthOverrideService; + + private constructor() {} + + static getInstance(): SharedHostAuthOverrideService { + if (!this.instance) { + this.instance = new SharedHostAuthOverrideService(); + } + return this.instance; + } + + async getCredentialId( + hostId: number, + userId: string, + protocol: AuthOverrideProtocol, + ): Promise { + this.requireSupportedProtocol(protocol); + await this.requireSharedHostAccess(hostId, userId); + return createCurrentSharedHostAuthOverrideRepository().findCredentialId( + hostId, + userId, + protocol, + ); + } + + async setCredentialId( + hostId: number, + userId: string, + protocol: AuthOverrideProtocol, + credentialId: number | null, + auditContext: SharedHostAuthOverrideAuditContext = {}, + ): Promise { + this.requireSupportedProtocol(protocol); + await this.requireSharedHostAccess(hostId, userId); + + if (credentialId !== null) { + const credential = + await createCurrentCredentialRepository().findByIdForUser( + userId, + credentialId, + ); + if (!credential) { + throw new SharedHostAuthOverrideServiceError( + "Credential not found", + 404, + ); + } + } + + const repository = createCurrentSharedHostAuthOverrideRepository(); + if (credentialId === null) { + await repository.clearCredential(hostId, userId, protocol); + } else { + await repository.setCredential(hostId, userId, protocol, credentialId); + } + + try { + const actor = await createCurrentUserRepository().findById(userId); + await logAudit({ + userId, + username: actor?.username ?? userId, + action: + credentialId === null + ? "clear_shared_host_auth_override" + : "set_shared_host_auth_override", + resourceType: "host", + resourceId: String(hostId), + details: JSON.stringify({ + protocol, + credentialId, + }), + ipAddress: auditContext.ipAddress, + userAgent: auditContext.userAgent, + success: true, + }); + } catch { + // Audit bookkeeping must never turn a successful override write into a + // failed API response. + } + } + + private requireSupportedProtocol(protocol: AuthOverrideProtocol): void { + if (!isSupportedAuthOverrideProtocol(protocol)) { + throw new SharedHostAuthOverrideServiceError( + `${AUTH_PROTOCOL_METADATA[protocol].label} authentication overrides are not supported yet`, + 400, + ); + } + } + + private async requireSharedHostAccess( + hostId: number, + userId: string, + ): Promise { + const access = await PermissionManager.getInstance().canAccessHost( + userId, + hostId, + "connect", + ); + if (!access.hasAccess || !access.isShared || access.isAdminBypass) { + throw new SharedHostAuthOverrideServiceError( + "Authentication overrides require active shared host access", + 403, + ); + } + } +} diff --git a/src/backend/utils/shared-host-auth-resolver.ts b/src/backend/utils/shared-host-auth-resolver.ts new file mode 100644 index 0000000..fd179c4 --- /dev/null +++ b/src/backend/utils/shared-host-auth-resolver.ts @@ -0,0 +1,130 @@ +import { + isSupportedAuthOverrideProtocol, + type AuthOverrideProtocol, +} from "../../types/auth-protocols.js"; +import { + createCurrentHostResolutionRepository, + createCurrentSharedHostAuthOverrideRepository, +} from "../database/repositories/factory.js"; +import type { + HostResolutionCredentialRecord, + HostResolutionHostRecord, +} from "../database/repositories/host-resolution-repository.js"; +import { + SharedHostSecretsManager, + type SharedSecretData, +} from "./shared-host-secrets-manager.js"; + +export type RecipientSharedHostAuthResolution = + | { + source: "personal-override"; + credentialId: number; + credential: HostResolutionCredentialRecord; + } + | { + source: "owner-shared"; + authType: string; + secret: SharedSecretData | null; + } + | { source: "secretless" } + | { source: "required" }; + +export function requiresPersonalHostAuthentication( + host: Pick, + protocol: AuthOverrideProtocol, +): boolean { + switch (protocol) { + case "ssh": + return ( + !!host.credentialId || + host.authType === "password" || + host.authType === "key" || + host.authType === "credential" || + host.authType === "agent" + ); + // These cases document the extension point without enabling behavior. + case "rdp": + case "vnc": + case "telnet": + throw new Error( + `${protocol.toUpperCase()} shared-host authentication is not implemented`, + ); + } +} + +/** + * Applies the shared-host authentication precedence independently from any + * transport: recipient override, explicitly shared owner auth, secretless + * auth, then "required". Only SSH is currently enabled by callers. + */ +export async function resolveRecipientSharedHostAuthentication( + host: HostResolutionHostRecord, + hostId: number, + userId: string, + protocol: AuthOverrideProtocol, +): Promise { + if (!isSupportedAuthOverrideProtocol(protocol)) { + throw new Error( + `${protocol.toUpperCase()} shared-host authentication is not implemented`, + ); + } + + const repository = createCurrentHostResolutionRepository(); + let overrideCredentialId: number | null = null; + try { + overrideCredentialId = + await createCurrentSharedHostAuthOverrideRepository().findCredentialId( + hostId, + userId, + protocol, + ); + } catch { + // A missing/deleted override behaves like no personal credential. + } + + if (overrideCredentialId) { + const credential = await repository.findCredentialByIdForUser( + overrideCredentialId, + userId, + ); + if (credential) { + return { + source: "personal-override", + credentialId: overrideCredentialId, + credential, + }; + } + } + + if (protocol === "ssh" && host.shareSshAuth) { + if (host.authType === "agent") { + return { + source: "owner-shared", + authType: "agent", + secret: null, + }; + } + + try { + const secret = + await SharedHostSecretsManager.getInstance().getSecretForUser( + hostId, + userId, + protocol, + ); + if (secret) { + return { + source: "owner-shared", + authType: secret.authType, + secret, + }; + } + } catch { + // An unreadable owner snapshot cannot expose the owner's auth. + } + } + + return requiresPersonalHostAuthentication(host, protocol) + ? { source: "required" } + : { source: "secretless" }; +} diff --git a/src/backend/utils/shared-host-secrets-manager.ts b/src/backend/utils/shared-host-secrets-manager.ts index be70acd..011f74a 100644 --- a/src/backend/utils/shared-host-secrets-manager.ts +++ b/src/backend/utils/shared-host-secrets-manager.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "./error-message.js"; import { createCurrentHostResolutionRepository, createCurrentRbacAccessRepository, @@ -57,9 +58,9 @@ function enabledProtocols( }; } -// Per-recipient copies of a shared host's connection secrets, re-encrypted -// under the recipient's DEK. Every enabled protocol gets its own snapshot; -// secret-less auth types (opkssh, vault, agent, none, ...) produce none. +// Per-recipient copies of connection secrets, re-encrypted under the +// recipient's DEK. SSH authentication is copied only when the host owner +// explicitly opts in; recipient-owned credential overrides remain separate. class SharedHostSecretsManager { private static instance: SharedHostSecretsManager; @@ -242,7 +243,7 @@ class SharedHostSecretsManager { hostId, hostAccessId: grant.id, targetUserId, - error: error instanceof Error ? error.message : "Unknown error", + error: getErrorMessage(error), }, ); } @@ -366,7 +367,7 @@ class SharedHostSecretsManager { const enabled = enabledProtocols(host); const snapshots: ProtocolSnapshot[] = []; - if (enabled.ssh) { + if (enabled.ssh && host.shareSshAuth) { if (host.credentialId) { const credential = await repository.findCredentialByIdForUser( host.credentialId, diff --git a/src/backend/utils/socks5-helper.ts b/src/backend/utils/socks5-helper.ts index 4799c2f..97975e8 100644 --- a/src/backend/utils/socks5-helper.ts +++ b/src/backend/utils/socks5-helper.ts @@ -1,6 +1,5 @@ export { createSocks5Connection, - createProxyConnection, createHttpConnectConnection, createMixedProxyChainConnection, testProxyConnectivity, diff --git a/src/backend/utils/ssh-algorithms.ts b/src/backend/utils/ssh-algorithms.ts index 40a5912..ca64393 100644 --- a/src/backend/utils/ssh-algorithms.ts +++ b/src/backend/utils/ssh-algorithms.ts @@ -31,13 +31,13 @@ try { nativeRequire("ssh2/lib/protocol/crypto/build/Release/sshcrypto.node"); ssh2BindingAvailable = true; } catch { - try { - // ESM fallback: check if chacha20 works via OpenSSL createCipheriv - crypto.createCipheriv("chacha20", Buffer.alloc(32), Buffer.alloc(16)); - ssh2BindingAvailable = true; - } catch { - ssh2BindingAvailable = false; - } + // The pure-JS fallback in ssh2 for chacha20-poly1305@openssh.com is broken and + // corrupts the transport: the target sshd aborts the KEX with + // "ssh_dispatch_run_fatal: ... incomplete message [preauth]" and the client times out. + // A working OpenSSL "chacha20" cipher is NOT sufficient here โ€” only the native + // binding (sshcrypto.node) makes chacha20-poly1305 usable. Keep it disabled otherwise + // so filterCiphers() drops it and the connection negotiates AES-GCM instead. + ssh2BindingAvailable = false; } function filterCiphers(list: CipherAlgorithm[]): CipherAlgorithm[] { diff --git a/src/backend/utils/ssh-key-utils.ts b/src/backend/utils/ssh-key-utils.ts index 2234d78..2c1df5e 100644 --- a/src/backend/utils/ssh-key-utils.ts +++ b/src/backend/utils/ssh-key-utils.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "./error-message.js"; import ssh2Pkg from "ssh2"; const ssh2Utils = ssh2Pkg.utils; @@ -337,7 +338,7 @@ export function parseSSHKey( // expected - fallback key type detection may fail } - const parserError = error instanceof Error ? error.message : ""; + const parserError = getErrorMessage(error, ""); const isPuttyKey = PUTTY_PRIVATE_KEY_RE.test(privateKeyData.trim()); return { @@ -367,26 +368,11 @@ export function parsePublicKey(publicKeyData: string): PublicKeyInfo { publicKey: publicKeyData, keyType: "unknown", success: false, - error: - error instanceof Error - ? error.message - : "Unknown error parsing public key", + error: getErrorMessage(error, "Unknown error parsing public key"), }; } } -export function detectKeyType(privateKeyData: string): string { - try { - const parsedKey = ssh2Utils.parseKey(privateKeyData); - if (parsedKey instanceof Error) { - return "unknown"; - } - return parsedKey.type || "unknown"; - } catch { - return "unknown"; - } -} - export function getFriendlyKeyTypeName(keyType: string): string { const keyTypeMap: Record = { "ssh-rsa": "RSA", @@ -481,10 +467,7 @@ export function validateKeyPair( isValid: false, privateKeyType: "unknown", publicKeyType: "unknown", - error: - error instanceof Error - ? error.message - : "Unknown error during validation", + error: getErrorMessage(error, "Unknown error during validation"), }; } } diff --git a/src/backend/utils/swagger.ts b/src/backend/utils/swagger.ts index 83a4367..a9fb264 100644 --- a/src/backend/utils/swagger.ts +++ b/src/backend/utils/swagger.ts @@ -71,6 +71,10 @@ const swaggerOptions: SwaggerJSDocOptions = { }, ], tags: [ + { + name: "AI", + description: "AI assistant providers, conversations and proposals", + }, { name: "Alerts", description: "System alerts and notifications management", @@ -119,12 +123,113 @@ const swaggerOptions: SwaggerJSDocOptions = { name: "File Manager", description: "SSH file management operations", }, + { + name: "SSH", + description: "SSH host management and configuration", + }, + { + name: "Host Enrollment", + description: "Host enrollment and onboarding", + }, + { + name: "Fleets", + description: "Fleet grouping, membership, and inventory", + }, + { + name: "Workspaces", + description: "Saved tab and split layouts", + }, + { + name: "Open Tabs", + description: "Per-user open tab state", + }, + { + name: "Automations", + description: "Scheduled and triggered automations", + }, + { + name: "Guacamole", + description: "RDP, VNC, and Telnet remote desktop sessions", + }, + { + name: "Proxmox", + description: "Proxmox host integration", + }, + { + name: "Proxmox Stats", + description: "Proxmox node and VM statistics", + }, + { + name: "Session Sharing", + description: "Live terminal session collaboration", + }, + { + name: "Session Logs", + description: "Session recording and playback", + }, + { + name: "Homepage", + description: "Homepage service links and layout", + }, + { + name: "Audit", + description: "Audit log querying and export", + }, + { + name: "API Keys", + description: "API key management", + }, + { + name: "SSO", + description: "Single sign-on provider configuration", + }, + { + name: "WebAuthn", + description: "Passkey registration and authentication", + }, + { + name: "Vault", + description: "HashiCorp Vault SSH signing profiles", + }, + { + name: "Termix ID", + description: "Built-in SSH certificate authority", + }, + { + name: "Tailscale", + description: "Tailscale network integration", + }, + { + name: "Sync", + description: "Remote sync between desktop and server", + }, + { + name: "Tunnel Presets", + description: "Saved tunnel configurations", + }, + { + name: "User Preferences", + description: "Per-user application preferences", + }, + { + name: "UI Preferences", + description: "Interface layout and display preferences", + }, + { + name: "Host Sidebar", + description: "Host sidebar display preferences", + }, + { + name: "Credential Sidebar", + description: "Credential sidebar display preferences", + }, ], }, apis: [ path .join(__dirname, "..", "database", "routes", "*.js") .replace(/\\/g, "/"), + path.join(__dirname, "..", "ai", "*.js").replace(/\\/g, "/"), path.join(__dirname, "..", "services", "*.js").replace(/\\/g, "/"), path.join(__dirname, "..", "hosts", "*.js").replace(/\\/g, "/"), path.join(__dirname, "..", "hosts", "**", "*.js").replace(/\\/g, "/"), diff --git a/src/backend/utils/system-secret-crypto.ts b/src/backend/utils/system-secret-crypto.ts new file mode 100644 index 0000000..b177228 --- /dev/null +++ b/src/backend/utils/system-secret-crypto.ts @@ -0,0 +1,121 @@ +import crypto from "crypto"; +import { SystemCrypto } from "./system-crypto.js"; + +/** + * Encryption for secrets that belong to the installation rather than to a user. + * + * Per-user field encryption (DataCrypto/FieldCrypto) derives its key from the + * user's DEK, which works for host passwords and SSH keys. It does not work for + * SSO provider configuration: `sso_providers` has no `userId`, and the OIDC + * client secret and LDAP bind password must be readable during login โ€” before + * any user is authenticated, let alone unlocked. + * + * Those secrets were previously stored base64-encoded behind an `encoded:` + * prefix, which is not encryption. This uses the system encryption key, the + * same one already protecting other installation-level material. + */ + +const ALGORITHM = "aes-256-gcm"; +const IV_LENGTH = 12; +const PREFIX = "sysenc:v1:"; +const LEGACY_PREFIX = "encoded:"; +/** Written by an older path that base64-encoded behind an "encrypted:" prefix. */ +const LEGACY_MISLABELLED_PREFIX = "encrypted:"; + +export function isSystemEncrypted(value: string): boolean { + return value.startsWith(PREFIX); +} + +export async function encryptSystemSecret(plaintext: string): Promise { + if (!plaintext) return plaintext; + if (isSystemEncrypted(plaintext)) return plaintext; + + const key = await SystemCrypto.getInstance().getEncryptionKey(); + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + const encrypted = Buffer.concat([ + cipher.update(plaintext, "utf8"), + cipher.final(), + ]); + const tag = cipher.getAuthTag(); + + return `${PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`; +} + +/** + * Reads a stored secret, transparently handling values written before this + * existed. Legacy values are returned as plaintext so login keeps working; they + * are upgraded on the next write. + */ +export async function decryptSystemSecret(stored: string): Promise { + if (!stored) return stored; + + if (!isSystemEncrypted(stored)) { + for (const legacy of [LEGACY_PREFIX, LEGACY_MISLABELLED_PREFIX]) { + if (stored.startsWith(legacy)) { + try { + return Buffer.from(stored.slice(legacy.length), "base64").toString( + "utf8", + ); + } catch { + return stored; + } + } + } + // Never encoded at all. + return stored; + } + + const [ivPart, tagPart, dataPart] = stored.slice(PREFIX.length).split(":"); + if (!ivPart || !tagPart || !dataPart) { + throw new Error("Malformed system-encrypted secret"); + } + + const key = await SystemCrypto.getInstance().getEncryptionKey(); + const decipher = crypto.createDecipheriv( + ALGORITHM, + key, + Buffer.from(ivPart, "base64"), + ); + decipher.setAuthTag(Buffer.from(tagPart, "base64")); + + return Buffer.concat([ + decipher.update(Buffer.from(dataPart, "base64")), + decipher.final(), + ]).toString("utf8"); +} + +/** Fields inside an SSO provider config that must not be stored readable. */ +export const SSO_SECRET_FIELDS = ["client_secret", "bindPassword"] as const; + +export async function encryptSsoConfigSecrets( + config: Record, +): Promise> { + const out = { ...config }; + for (const field of SSO_SECRET_FIELDS) { + const value = out[field]; + if (typeof value === "string" && value) { + out[field] = await encryptSystemSecret(value); + } + } + return out; +} + +export async function decryptSsoConfigSecrets( + config: Record, +): Promise> { + const out = { ...config }; + for (const field of SSO_SECRET_FIELDS) { + const value = out[field]; + if (typeof value === "string" && value) { + try { + out[field] = await decryptSystemSecret(value); + } catch { + // A secret we cannot read must not take the whole provider down; + // login will fail with a clearer error downstream. + } + } + } + return out; +} diff --git a/src/backend/utils/trusted-proxy-auth.ts b/src/backend/utils/trusted-proxy-auth.ts new file mode 100644 index 0000000..a2e207b --- /dev/null +++ b/src/backend/utils/trusted-proxy-auth.ts @@ -0,0 +1,131 @@ +import { BlockList, isIP } from "node:net"; + +export interface TrustedProxyAuthConfig { + enabled: boolean; + usernameHeader: string; + roleHeader: string; + trustedProxies: string[]; + roleMap: Map; +} + +function enabled(value: string | undefined): boolean { + return value?.trim().toLowerCase() === "true"; +} + +export function parseTrustedProxyRoleMap( + value: string | undefined, +): Map { + if (!value?.trim()) return new Map(); + const parsed = JSON.parse(value) as Record; + const result = new Map(); + for (const [externalRole, mapped] of Object.entries(parsed)) { + const roles = (Array.isArray(mapped) ? mapped : [mapped]) + .filter((role): role is string => typeof role === "string") + .map((role) => role.trim()) + .filter(Boolean); + if (externalRole.trim() && roles.length > 0) { + result.set(externalRole.trim(), [...new Set(roles)]); + } + } + return result; +} + +export function getTrustedProxyAuthConfig( + env: NodeJS.ProcessEnv = process.env, +): TrustedProxyAuthConfig { + const config = { + enabled: enabled(env.TRUSTED_PROXY_AUTH_ENABLED), + usernameHeader: ( + env.TRUSTED_PROXY_AUTH_USERNAME_HEADER || "x-forwarded-username" + ).toLowerCase(), + roleHeader: ( + env.TRUSTED_PROXY_AUTH_ROLE_HEADER || "x-forwarded-role" + ).toLowerCase(), + trustedProxies: (env.TRUSTED_PROXY_AUTH_TRUSTED_PROXIES || "") + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean), + roleMap: parseTrustedProxyRoleMap(env.TRUSTED_PROXY_AUTH_ROLE_MAP), + }; + + if ( + config.enabled && + (config.trustedProxies.length === 0 || config.roleMap.size === 0) + ) { + throw new Error( + "Trusted proxy auth requires TRUSTED_PROXY_AUTH_TRUSTED_PROXIES and TRUSTED_PROXY_AUTH_ROLE_MAP", + ); + } + if ( + config.enabled && + (!/^[a-z0-9-]+$/.test(config.usernameHeader) || + !/^[a-z0-9-]+$/.test(config.roleHeader)) + ) { + throw new Error("Trusted proxy auth header names are invalid"); + } + if (config.enabled) { + // Build the allowlist during configuration parsing so an invalid CIDR + // fails startup rather than surfacing on the first login attempt. + isTrustedProxyAddress("127.0.0.1", config.trustedProxies); + } + return config; +} + +function normalizeAddress(address: string): string { + const withoutZone = address.split("%")[0]; + return withoutZone.startsWith("::ffff:") + ? withoutZone.slice("::ffff:".length) + : withoutZone; +} + +export function isTrustedProxyAddress( + address: string | undefined, + trustedProxies: string[], +): boolean { + if (!address) return false; + const blockList = new BlockList(); + for (const entry of trustedProxies) { + const [rawAddress, rawPrefix] = entry.split("/"); + const normalized = normalizeAddress(rawAddress); + const family = isIP(normalized); + if (!family) throw new Error(`Invalid trusted proxy address: ${entry}`); + const type = family === 4 ? "ipv4" : "ipv6"; + if (rawPrefix === undefined) { + blockList.addAddress(normalized, type); + continue; + } + const prefix = Number(rawPrefix); + const max = family === 4 ? 32 : 128; + if (!Number.isInteger(prefix) || prefix < 0 || prefix > max) { + throw new Error(`Invalid trusted proxy CIDR: ${entry}`); + } + blockList.addSubnet(normalized, prefix, type); + } + const normalized = normalizeAddress(address); + const family = isIP(normalized); + return ( + family !== 0 && blockList.check(normalized, family === 4 ? "ipv4" : "ipv6") + ); +} + +export function resolveTrustedProxyRoles( + header: string, + roleMap: Map, +): string[] | null { + const externalRoles = header + .split(",") + .map((role) => role.trim()) + .filter(Boolean); + if (externalRoles.length === 0) return null; + const resolved = new Set(); + for (const externalRole of externalRoles) { + const mapped = roleMap.get(externalRole); + if (!mapped) return null; + mapped.forEach((role) => resolved.add(role)); + } + return [...resolved]; +} + +export function isTrustedProxyAuthEnabled(): boolean { + return enabled(process.env.TRUSTED_PROXY_AUTH_ENABLED); +} diff --git a/src/backend/utils/user-agent-parser.ts b/src/backend/utils/user-agent-parser.ts index b0644ec..cbd9db1 100644 --- a/src/backend/utils/user-agent-parser.ts +++ b/src/backend/utils/user-agent-parser.ts @@ -250,17 +250,21 @@ function parseMacVersion(userAgent: string): string { return "macOS"; } -/** - * Generate a stable device fingerprint based on device type, browser, and OS. - * Ignores minor version numbers to handle browser auto-updates. - */ -export function generateDeviceFingerprint(deviceInfo: DeviceInfo): string { - const fingerprintString = - deviceInfo.type === "desktop" || deviceInfo.type === "mobile" - ? `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}` - : `${deviceInfo.type}|${deviceInfo.browser} ${ - deviceInfo.version.split(".")[0] - }|${deviceInfo.os}`; +/** Return the installation-scoped identifier used for trusted-device checks. */ +export function getDeviceId(req: Request): string | null { + const value = req.headers["x-termix-device-id"]; + if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) return null; + return value; +} - return crypto.createHash("sha256").update(fingerprintString).digest("hex"); +/** Bind a trusted-device record to one client installation and platform. */ +export function generateDeviceFingerprint( + deviceInfo: DeviceInfo, + deviceId: string | null, +): string | null { + if (!deviceId) return null; + return crypto + .createHash("sha256") + .update(`${deviceInfo.type}|${deviceId}`) + .digest("hex"); } diff --git a/src/main.tsx b/src/main.tsx index 1e4c889..aa0d961 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,6 +14,8 @@ import { installElectronWheelZoomGuard } from "@/lib/electron-wheel-zoom"; import type { FontSizeId } from "@/types/ui-types"; import { useServiceWorker } from "@/hooks/use-service-worker"; import { useTranslation } from "react-i18next"; +import { UiPreferencesProvider } from "@/contexts/UiPreferencesContext"; +import { ConnectionDefaultsProvider } from "@/contexts/ConnectionDefaultsContext"; const AppShell = lazy(() => import("@/AppShell").then((m) => ({ default: m.AppShell })), @@ -38,6 +40,11 @@ const HostMetricsApp = lazy(() => default: m.default, })), ); +const ProxmoxStatsApp = lazy(() => + import("@/features/proxmox-stats/ProxmoxStatsApp").then((m) => ({ + default: m.default, + })), +); const DockerApp = lazy(() => import("@/features/docker/DockerApp").then((m) => ({ default: m.default })), ); @@ -65,12 +72,14 @@ const ElectronVersionCheck = lazy(() => })), ); +// Anonymous guest view for shared terminal/RDP/VNC/Telnet sessions (?view=shared&token=). +// Rendered outside FullscreenAppGate since guests never have a JWT/cookie to verify. +const SharedSessionView = lazy( + () => import("@/features/session-sharing/SharedSessionView"), +); + type Phase = - | "verifying" - | "idle-auth" - | "fading-in" - | "idle-app" - | "fading-out"; + "verifying" | "idle-auth" | "fading-in" | "idle-app" | "fading-out"; function FullscreenApp() { const searchParams = new URLSearchParams(window.location.search); @@ -99,6 +108,8 @@ function FullscreenApp() { case "host-metrics": case "server-stats": return ; + case "proxmox-stats": + return ; case "docker": return ; case "rdp": @@ -174,11 +185,15 @@ function App() { stored?.loggedIn ? "verifying" : "idle-auth", ); const [authUsername, setAuthUsername] = useState(stored?.username ?? ""); + const [verifyRetryCount, setVerifyRetryCount] = useState(0); const timerRef = useRef | null>(null); // Track whether fading-in came from a fresh login (vs. session verification on page load). // When session-verified, Auth must not mount during the transition โ€” it would trigger // silent OIDC redirect and cause an infinite refresh loop. const fadingInFromLoginRef = useRef(false); + // Dedupes concurrent handleLogout() calls within the same tick -- see + // handleLogout for why phase state alone isn't sufficient for this. + const loggingOutRef = useRef(false); useEffect(() => { const savedAccent = localStorage.getItem("termix-accent"); @@ -186,7 +201,7 @@ function App() { const savedSize = localStorage.getItem( "termix-font-size", ) as FontSizeId | null; - applyFontSize(savedSize ?? "lg"); + applyFontSize(savedSize ?? "md"); return () => { if (timerRef.current) clearTimeout(timerRef.current); }; @@ -204,7 +219,18 @@ function App() { if (isElectron()) { try { const token = await getCurrentToken(); - if (token) localStorage.setItem("jwt", token); + if (token) { + localStorage.setItem("jwt", token); + // Remote Sync's engine (main process) needs this local JWT to + // authenticate against the embedded backend during sync, same + // as a fresh login provides via handleLogin below -- a session + // restore (the common case on every normal launch) must hand + // it over too, or sync silently never runs after the first + // app restart. + window.electronAPI + ?.invoke?.("notify-local-login", token) + .catch(() => {}); + } } catch { // Non-fatal: WebSocket connections will fall back to cookie auth } @@ -213,36 +239,83 @@ function App() { setPhase("fading-in"); timerRef.current = setTimeout(() => setPhase("idle-app"), 450); }) - .catch(() => { - clearStoredAuth(); - setPhase("idle-auth"); + .catch((err: unknown) => { + // Only treat a genuine auth rejection (401/403) as "not logged in". + // Anything else (network hiccup, backend still starting up, a + // transient 5xx) is not proof the session is invalid -- clearing + // stored auth here would drop the user back to Auth.tsx, which in + // Electron immediately mints a brand-new auto-session, silently + // swapping out the JWT/cookie from under any still-in-flight + // requests and causing spurious "Session expired" toasts. + const status = + (err as { status?: number; response?: { status?: number } }) + ?.status ?? + (err as { response?: { status?: number } })?.response?.status; + if (status === 401 || status === 403) { + clearStoredAuth(); + setPhase("idle-auth"); + return; + } + // Transient failure: retry rather than logging out. In Electron the + // embedded local backend is bundled, always-on infrastructure that + // always eventually comes up (a slow cold boot just takes longer), + // and Auth.tsx never shows a login form for it anyway -- so there's + // no reason to ever give up and manufacture a logout here. Outside + // Electron a genuinely broken backend still needs to surface the + // login screen eventually, so that case keeps a retry cap. + if (!isElectron() && verifyRetryCount >= 5) { + clearStoredAuth(); + setPhase("idle-auth"); + return; + } + const delay = isElectron() + ? Math.min(1000 * 2 ** verifyRetryCount, 10000) + : 3000; + timerRef.current = setTimeout(() => { + setVerifyRetryCount((c) => c + 1); + }, delay); }); - }, [phase]); + }, [phase, verifyRetryCount]); function handleLogin(u: string) { + loggingOutRef.current = false; setAuthUsername(u); fadingInFromLoginRef.current = true; setPhase("fading-in"); timerRef.current = setTimeout(() => setPhase("idle-app"), 450); if (isElectron()) { window.electronAPI?.startC2SAutoStartTunnels?.().catch(() => {}); + const localJwt = localStorage.getItem("jwt"); + if (localJwt) { + window.electronAPI + ?.invoke?.("notify-local-login", localJwt) + .catch(() => {}); + } } } function handleLogout() { + // A single background hiccup can trigger several independent 401s at + // once (e.g. a burst of unrelated polls all failing together in the + // same tick), each calling this. React batches the resulting setPhase + // calls, so checking `phase` here can't distinguish the first call in + // a batch from the second -- both would see the same pre-update value + // and both would proceed, each overwriting timerRef with a fresh + // 450ms timer. A steady trickle of these could keep resetting the + // countdown so the transition never actually completes, which looks + // exactly like "nothing happens." loggingOutRef is synchronous and + // isn't subject to batching, so it correctly dedupes within one tick. + if (loggingOutRef.current) return; + loggingOutRef.current = true; clearStoredAuth(); setPhase("fading-out"); timerRef.current = setTimeout(() => { setAuthUsername(""); setPhase("idle-auth"); + loggingOutRef.current = false; }, 450); } - function handleChangeServer() { - localStorage.setItem("termix_show_server_config", "true"); - handleLogout(); - } - const showApp = phase === "idle-app" || phase === "fading-in" || phase === "fading-out"; const showAuth = @@ -288,11 +361,11 @@ function App() { }} > - + + + + + )} @@ -322,10 +395,24 @@ function RootApp() { const searchParams = new URLSearchParams(window.location.search); const isFullscreen = searchParams.has("view"); + // Anonymous guests have no cookie/JWT at all, so this bypasses FullscreenAppGate's + // auth check entirely rather than waiting on a getUserInfo() call that would always fail. + if (searchParams.get("view") === "shared") { + return ( + + + + ); + } + if (isFullscreen) { return ( - + + + + + ); } diff --git a/src/types/auth-protocols.ts b/src/types/auth-protocols.ts new file mode 100644 index 0000000..664f200 --- /dev/null +++ b/src/types/auth-protocols.ts @@ -0,0 +1,71 @@ +export const AUTH_OVERRIDE_PROTOCOLS = ["ssh", "rdp", "vnc", "telnet"] as const; + +export type AuthOverrideProtocol = (typeof AUTH_OVERRIDE_PROTOCOLS)[number]; + +// Storage and API contracts are protocol-aware, but SSH is intentionally the +// only protocol whose recipient override flow is enabled today. +export const SUPPORTED_AUTH_OVERRIDE_PROTOCOLS = [ + "ssh", +] as const satisfies readonly AuthOverrideProtocol[]; + +export const AUTH_PROTOCOL_METADATA = { + ssh: { + label: "SSH", + enableField: "enableSsh", + credentialField: "credentialId", + }, + rdp: { + label: "RDP", + enableField: "enableRdp", + credentialField: "rdpCredentialId", + }, + vnc: { + label: "VNC", + enableField: "enableVnc", + credentialField: "vncCredentialId", + }, + telnet: { + label: "Telnet", + enableField: "enableTelnet", + credentialField: "telnetCredentialId", + }, +} as const satisfies Record< + AuthOverrideProtocol, + { + label: string; + enableField: "enableSsh" | "enableRdp" | "enableVnc" | "enableTelnet"; + credentialField: + | "credentialId" + | "rdpCredentialId" + | "vncCredentialId" + | "telnetCredentialId"; + } +>; + +export function isAuthOverrideProtocol( + value: unknown, +): value is AuthOverrideProtocol { + return ( + typeof value === "string" && + AUTH_OVERRIDE_PROTOCOLS.includes(value as AuthOverrideProtocol) + ); +} + +export function isSupportedAuthOverrideProtocol( + protocol: AuthOverrideProtocol, +): boolean { + return SUPPORTED_AUTH_OVERRIDE_PROTOCOLS.includes( + protocol as (typeof SUPPORTED_AUTH_OVERRIDE_PROTOCOLS)[number], + ); +} + +export interface HostAuthOverrideState< + CredentialId extends number | string = number, +> { + credentialId?: CredentialId; + required: boolean; + ownerAuthShared: boolean; +} + +export type HostAuthOverrides = + Partial>>; diff --git a/src/types/automations.ts b/src/types/automations.ts new file mode 100644 index 0000000..02a1399 --- /dev/null +++ b/src/types/automations.ts @@ -0,0 +1,286 @@ +/** + * Shared automation model used by both the backend engine and the editor UI. + * An automation is one trigger plus an ordered list of steps. The whole + * definition is stored as JSON on the automations row, so this file is the + * only contract describing that blob. + */ + +export const AUTOMATION_DEFINITION_VERSION = 1; + +/** Which hosts a trigger watches or a step acts on. */ +export type HostSelector = + | { kind: "host"; hostId: number } + | { kind: "hosts"; hostIds: number[] } + | { kind: "fleet"; fleetId: number } + | { kind: "all" } + /** The host that produced the event. Only valid for host-scoped triggers. */ + | { kind: "trigger" }; + +/** + * A metric to compare against. Paths mirror the shape returned by + * collectMetrics(). mount/iface pick one entry out of a per-instance list so a + * rule can watch a single filesystem rather than the aggregate. + */ +export type MetricPath = + | { path: "cpu.percent" } + | { path: "cpu.load1" } + | { path: "cpu.load5" } + | { path: "cpu.load15" } + | { path: "memory.percent" } + | { path: "memory.usedGiB" } + | { path: "disk.percent"; mount?: string } + | { path: "disk.availableBytes"; mount?: string } + | { path: "temperature.highestCelsius" } + | { path: "uptime.seconds" } + | { path: "processes.total" } + | { path: "network.rxBytes"; iface?: string } + | { path: "network.txBytes"; iface?: string } + | { path: "network.rxRateBps"; iface?: string } + | { path: "network.txRateBps"; iface?: string }; + +export type Operator = + | ">" + | "<" + | ">=" + | "<=" + | "==" + | "!=" + | "contains" + | "not_contains" + | "changed"; + +export type Severity = "info" | "warning" | "critical"; + +export type HostStatusState = "online" | "offline"; +export type HealthCheckState = "failing" | "recovered"; +export type DockerEventKind = "exited" | "started" | "unhealthy" | "restarting"; + +export type InternalEventKind = + | "user_login" + | "host_added" + | "host_deleted" + | "tunnel_disconnected" + | "automation_failed"; + +export type Trigger = + | { + kind: "metric_threshold"; + hostSelector: HostSelector; + metric: MetricPath; + operator: Operator; + value: number; + /** Sustained breach window before firing. 0 fires immediately. */ + forSeconds?: number; + cooldownMinutes: number; + severity?: Severity; + } + | { + kind: "host_status"; + hostSelector: HostSelector; + to: HostStatusState; + cooldownMinutes: number; + } + | { + kind: "health_check"; + hostSelector: HostSelector; + checkId?: string; + to: HealthCheckState; + cooldownMinutes: number; + } + | { + kind: "schedule"; + /** Five field cron. Ignored when intervalSeconds is set. */ + cron?: string; + intervalSeconds?: number; + timezone?: string; + } + | { + kind: "docker_event"; + hostSelector: HostSelector; + container?: string; + event: DockerEventKind; + cooldownMinutes: number; + } + | { + kind: "internal_event"; + event: InternalEventKind; + hostSelector?: HostSelector; + cooldownMinutes: number; + } + | { + kind: "webhook"; + /** Only the hash is persisted. The raw token is shown once on creation. */ + tokenHash: string; + }; + +export type TriggerKind = Trigger["kind"]; + +/** A comparison used by `if` steps, evaluated against the run context. */ +export interface Condition { + /** Template string, e.g. "{{steps.check.stdout}}" or "{{trigger.value}}". */ + left: string; + operator: Operator; + /** Template string. Compared numerically when both sides parse as numbers. */ + right?: string; +} + +export type StepErrorPolicy = "stop" | "continue" | "branch"; + +interface StepBase { + /** Stable across edits so run history survives reordering. */ + id: string; + name?: string; + enabled?: boolean; + onError?: StepErrorPolicy; + timeoutMs?: number; +} + +export type Step = + | (StepBase & { + type: "notify"; + channelIds: number[]; + title?: string; + body?: string; + severity?: Severity; + }) + | (StepBase & { + type: "http"; + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + url: string; + headers?: Record; + body?: string; + /** Opt in to LAN/private addresses. Off by default, see safe-outbound-fetch. */ + allowPrivateNetwork?: boolean; + }) + | (StepBase & { + type: "run_snippet"; + snippetId: number; + hostSelector: HostSelector; + inputValues?: Record; + elevated?: boolean; + }) + | (StepBase & { + type: "run_command"; + command: string; + hostSelector: HostSelector; + elevated?: boolean; + }) + | (StepBase & { + type: "docker"; + action: "start" | "stop" | "restart"; + container: string; + hostSelector: HostSelector; + }) + | (StepBase & { + type: "tunnel"; + action: "connect" | "disconnect"; + tunnelName: string; + }) + | (StepBase & { type: "wol"; hostId: number }) + | (StepBase & { type: "wait"; seconds: number }) + | (StepBase & { type: "set_var"; name: string; value: string }) + | (StepBase & { + type: "if"; + condition: Condition; + then: Step[]; + else?: Step[]; + }) + | (StepBase & { type: "run_automation"; automationId: number }) + | (StepBase & { type: "stop"; status?: "success" | "failed" }); + +export type StepType = Step["type"]; + +export interface AutomationDefinition { + version: number; + trigger: Trigger; + steps: Step[]; +} + +export type ConcurrencyPolicy = "skip" | "queue" | "allow"; + +export type RunStatus = + "running" | "success" | "failed" | "timeout" | "skipped" | "cancelled"; + +export type StepStatus = + "pending" | "running" | "success" | "failed" | "skipped"; + +export interface Automation { + id: number; + userId: string; + name: string; + description: string | null; + enabled: boolean; + definition: AutomationDefinition; + concurrencyPolicy: ConcurrencyPolicy; + maxRunSeconds: number; + dryRun: boolean; + lastRunAt: string | null; + lastRunStatus: RunStatus | null; + createdAt: string; + updatedAt: string; +} + +export interface AutomationRun { + id: number; + automationId: number; + userId: string; + triggerType: TriggerKind | "manual"; + triggerContext: Record | null; + status: RunStatus; + startedAt: string; + finishedAt: string | null; + durationMs: number | null; + error: string | null; + dryRun: boolean; + parentRunId: number | null; +} + +export interface AutomationRunStep { + id: number; + runId: number; + stepIndex: number; + stepId: string; + stepType: StepType; + status: StepStatus; + startedAt: string; + finishedAt: string | null; + output: string | null; + error: string | null; + truncated: boolean; +} + +/** Defaults applied when an automation does not override them. */ +export const DEFAULT_MAX_RUN_SECONDS = 300; +export const DEFAULT_STEP_TIMEOUT_MS = 60_000; +export const DEFAULT_COOLDOWN_MINUTES = 15; +/** Guards run_automation against direct and mutual recursion. */ +export const MAX_AUTOMATION_DEPTH = 5; +/** Step output beyond this is truncated before it reaches the database. */ +export const MAX_STEP_OUTPUT_BYTES = 32_768; + +export const OPERATOR_LABELS: Record = { + ">": "greater than", + "<": "less than", + ">=": "greater than or equal to", + "<=": "less than or equal to", + "==": "equals", + "!=": "does not equal", + contains: "contains", + not_contains: "does not contain", + changed: "changed", +}; + +/** Steps that reach outside Termix and are therefore stubbed in a dry run. */ +export const SIDE_EFFECTING_STEP_TYPES: readonly StepType[] = [ + "notify", + "http", + "run_snippet", + "run_command", + "docker", + "tunnel", + "wol", +]; + +export function isSideEffectingStep(type: StepType): boolean { + return SIDE_EFFECTING_STEP_TYPES.includes(type); +} diff --git a/src/types/connection-log.ts b/src/types/connection-log.ts index 62a9f04..fc8eae4 100644 --- a/src/types/connection-log.ts +++ b/src/types/connection-log.ts @@ -8,6 +8,7 @@ export type ConnectionStage = | "error" | "proxy" | "jump" + | "validation" | "docker_connecting" | "docker_auth" | "docker_session" @@ -24,7 +25,13 @@ export type ConnectionStage = | "tunnel_connected" | "sftp_connecting" | "sftp_auth" - | "sftp_connected"; + | "sftp_connected" + | "guac_token" + | "guac_guacd" + | "guac_connecting" + | "guac_handshake" + | "guac_ready" + | "guac_disconnected"; export type LogEntry = { id: string; @@ -32,7 +39,7 @@ export type LogEntry = { type: "info" | "success" | "warning" | "error"; stage: ConnectionStage; message: string; - details?: Record; + details?: Record | string; }; export interface ConnectionLogResponse { diff --git a/src/types/credential-sidebar-preferences.ts b/src/types/credential-sidebar-preferences.ts new file mode 100644 index 0000000..bd474a9 --- /dev/null +++ b/src/types/credential-sidebar-preferences.ts @@ -0,0 +1,144 @@ +/** + * Credential sidebar preferences model. Shared by the frontend sidebar and + * the backend preferences endpoint (no framework imports, mirrors + * ./host-sidebar-preferences.ts's dependency-free convention). Independent + * from HostSidebarPreferences by design โ€” a separate parallel system, not a + * shared blob, matching how credentialSortKey/credentialFilterState were + * already independently namespaced from hostSortKey/etc. before this port. + * + * Deliberately smaller than HostSidebarPreferences: no groupKey selector + * (folder is the only grouping credentials have, so there's nothing to + * pick), no statusColorScheme (credentials have no online/offline concept). + */ + +export const CREDENTIAL_SIDEBAR_PREFS_VERSION = 1; + +export type CredentialSortKey = + | "default" + | "name-asc" + | "name-desc" + | "username-asc" + | "username-desc" + | "manual"; + +export type CredentialDensity = "comfortable" | "compact"; + +export type CredentialTrayTrigger = + "always" | "hover" | "click" | "actionsOnly"; + +export interface CredentialSidebarFilterState { + type: ("password" | "key")[]; + tags: string[]; +} + +export interface CredentialSidebarDisplayPreferences { + density: CredentialDensity; + showTags: boolean; + trayTrigger: CredentialTrayTrigger; +} + +export interface CredentialSidebarPreferences { + version: number; + sort: { key: CredentialSortKey; pinnedFirst: boolean }; + filters: CredentialSidebarFilterState; + openFolders: string[]; + display: CredentialSidebarDisplayPreferences; +} + +const SORT_KEYS: CredentialSortKey[] = [ + "default", + "name-asc", + "name-desc", + "username-asc", + "username-desc", + "manual", +]; +const DENSITIES: CredentialDensity[] = ["comfortable", "compact"]; +const TRAY_TRIGGERS: CredentialTrayTrigger[] = [ + "always", + "hover", + "click", + "actionsOnly", +]; +const FILTER_TYPE: CredentialSidebarFilterState["type"] = ["password", "key"]; + +export function defaultCredentialSidebarPreferences(): CredentialSidebarPreferences { + return { + version: CREDENTIAL_SIDEBAR_PREFS_VERSION, + sort: { key: "default", pinnedFirst: false }, + filters: { + type: [], + tags: [], + }, + openFolders: [], + display: { + density: "comfortable", + showTags: true, + trayTrigger: "always", + }, + }; +} + +function sanitizeStringArray(input: unknown): string[] { + if (!Array.isArray(input)) return []; + return input.filter((v): v is string => typeof v === "string"); +} + +function sanitizeEnumArray( + input: unknown, + allowed: readonly T[], +): T[] { + if (!Array.isArray(input)) return []; + return input.filter((v): v is T => allowed.includes(v as T)); +} + +export function sanitizeCredentialSidebarPreferences( + input: unknown, +): CredentialSidebarPreferences { + const defaults = defaultCredentialSidebarPreferences(); + if (!input || typeof input !== "object") return defaults; + const obj = input as Record; + + const sortObj = (obj.sort ?? {}) as Record; + const sort = { + key: SORT_KEYS.includes(sortObj.key as CredentialSortKey) + ? (sortObj.key as CredentialSortKey) + : defaults.sort.key, + pinnedFirst: + typeof sortObj.pinnedFirst === "boolean" + ? sortObj.pinnedFirst + : defaults.sort.pinnedFirst, + }; + + const filtersObj = (obj.filters ?? {}) as Record; + const filters: CredentialSidebarFilterState = { + type: sanitizeEnumArray(filtersObj.type, FILTER_TYPE), + tags: sanitizeStringArray(filtersObj.tags), + }; + + const openFolders = sanitizeStringArray(obj.openFolders); + + const displayObj = (obj.display ?? {}) as Record; + const display: CredentialSidebarDisplayPreferences = { + density: DENSITIES.includes(displayObj.density as CredentialDensity) + ? (displayObj.density as CredentialDensity) + : defaults.display.density, + showTags: + typeof displayObj.showTags === "boolean" + ? displayObj.showTags + : defaults.display.showTags, + trayTrigger: TRAY_TRIGGERS.includes( + displayObj.trayTrigger as CredentialTrayTrigger, + ) + ? (displayObj.trayTrigger as CredentialTrayTrigger) + : defaults.display.trayTrigger, + }; + + return { + version: CREDENTIAL_SIDEBAR_PREFS_VERSION, + sort, + filters, + openFolders, + display, + }; +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 61a3818..1d1d62a 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -29,6 +29,12 @@ interface DialogResult { export interface ElectronAPI { getAppVersion: () => Promise; getPlatform: () => Promise; + openNativeRdp: (options: { + host: string; + port?: number; + username?: string; + domain?: string; + }) => Promise<{ success: boolean; error?: string }>; getSetting?: (key: string) => Promise; setSetting?: (key: string, value: string) => Promise; @@ -64,6 +70,16 @@ export interface ElectronAPI { started: number; errors: string[]; }>; + onRemoteSyncStatusChanged?: ( + callback: (status: { + connected: boolean; + syncing: boolean; + lastSyncedAt: string | null; + lastError: string | null; + needsReauth: boolean; + }) => void, + ) => () => void; + onCloseActiveTab?: (callback: () => void) => () => void; clearSessionCookies: () => Promise; getSessionCookie: ( name: string, @@ -151,13 +167,33 @@ export interface ElectronAPI { success: boolean; error?: string; }>; + + startLocalTerminal(dimensions: { + cols: number; + rows: number; + }): Promise<{ sessionId: string; shell: string }>; + readyLocalTerminal(sessionId: string): Promise; + writeLocalTerminal(sessionId: string, data: string): Promise; + resizeLocalTerminal( + sessionId: string, + cols: number, + rows: number, + ): Promise; + closeLocalTerminal(sessionId: string): Promise; + onLocalTerminalData( + sessionId: string, + callback: (data: string) => void, + ): () => void; + onLocalTerminalExit( + sessionId: string, + callback: (exitCode: number) => void, + ): () => void; } declare global { interface Window { electronAPI: ElectronAPI; IS_ELECTRON: boolean; - configuredServerUrl?: string | null; electronClipboard?: { writeText(text: string): Promise; readText(): Promise; diff --git a/src/types/guacamole-common-js.d.ts b/src/types/guacamole-common-js.d.ts index 3dbb36c..eefdeaf 100644 --- a/src/types/guacamole-common-js.d.ts +++ b/src/types/guacamole-common-js.d.ts @@ -14,6 +14,25 @@ declare module "guacamole-common-js" { onerror: ((error: Status) => void) | null; onclipboard: ((stream: InputStream, mimetype: string) => void) | null; onaudio: ((stream: InputStream, mimetype: string) => void) | null; + onfile: + | ((stream: InputStream, mimetype: string, filename: string) => void) + | null; + onfilesystem: ((filesystem: Object, name: string) => void) | null; + } + + // Mirrors Guacamole.Object: a named collection of streams. Within this + // namespace `Object` refers to this class, not the global one. + class Object { + static readonly ROOT_STREAM: string; + static readonly STREAM_INDEX_MIMETYPE: string; + readonly index: number; + requestInputStream( + name: string, + bodyCallback?: (stream: InputStream, mimetype: string) => void, + ): void; + createOutputStream(mimetype: string, name: string): OutputStream; + onbody: ((stream: InputStream, mimetype: string) => void) | null; + onundefine: (() => void) | null; } class AudioPlayer { @@ -49,8 +68,7 @@ declare module "guacamole-common-js" { onplay: (() => void) | null; onpause: (() => void) | null; onseek: - | ((position: number, current: number, total: number) => void) - | null; + ((position: number, current: number, total: number) => void) | null; getDisplay(): Display; getPosition(): number; getDuration(): number; @@ -97,10 +115,44 @@ declare module "guacamole-common-js" { up: boolean; down: boolean; } + + interface MouseEvent { + state: Mouse.State; + preventDefault(): void; + stopPropagation(): void; + } + + class Touchpad { + constructor(element: HTMLElement); + currentState: Mouse.State; + clickTimingThreshold: number; + clickMoveThreshold: number; + scrollThreshold: number; + onEach( + types: string[], + listener: (event: Mouse.MouseEvent) => void, + ): void; + on(type: string, listener: (event: Mouse.MouseEvent) => void): void; + } + + class Touchscreen { + constructor(element: HTMLElement); + currentState: Mouse.State; + clickTimingThreshold: number; + clickMoveThreshold: number; + scrollThreshold: number; + longPressThreshold: number; + onEach( + types: string[], + listener: (event: Mouse.MouseEvent) => void, + ): void; + on(type: string, listener: (event: Mouse.MouseEvent) => void): void; + } } class Keyboard { constructor(element: Document | HTMLElement); + reset(): void; onkeydown: ((keysym: number) => void) | null; onkeyup: ((keysym: number) => void) | null; } @@ -109,11 +161,27 @@ declare module "guacamole-common-js" { code: number; message: string; isError(): boolean; + static readonly Code: { + SUCCESS: number; + UNSUPPORTED: number; + SERVER_ERROR: number; + SERVER_BUSY: number; + UPSTREAM_TIMEOUT: number; + UPSTREAM_ERROR: number; + RESOURCE_NOT_FOUND: number; + RESOURCE_CONFLICT: number; + RESOURCE_CLOSED: number; + CLIENT_BAD_REQUEST: number; + CLIENT_UNAUTHORIZED: number; + CLIENT_FORBIDDEN: number; + CLIENT_TIMEOUT: number; + }; } class InputStream { onblob: ((data: string) => void) | null; onend: (() => void) | null; + sendAck(message: string, code: number): void; } class OutputStream { @@ -132,6 +200,24 @@ declare module "guacamole-common-js" { sendText(text: string): void; sendEnd(): void; } + + class BlobReader { + constructor(stream: InputStream, mimetype: string); + getBlob(): Blob; + getLength(): number; + onprogress: ((length: number) => void) | null; + onend: (() => void) | null; + } + + class BlobWriter { + constructor(stream: OutputStream); + sendBlob(blob: Blob): void; + sendEnd(): void; + onack: ((status: Status) => void) | null; + onerror: ((blob: Blob, offset: number, error: Status) => void) | null; + onprogress: ((blob: Blob, offset: number) => void) | null; + oncomplete: ((blob: Blob) => void) | null; + } } export default Guacamole; diff --git a/src/types/guacamole-config.ts b/src/types/guacamole-config.ts new file mode 100644 index 0000000..74e7b63 --- /dev/null +++ b/src/types/guacamole-config.ts @@ -0,0 +1,64 @@ +/** + * Per-host Guacamole connection settings, as edited in the host editor and + * forwarded to guacd. Field names are camelCase here and translated to the + * hyphenated protocol parameters in toGuacamoleParams(). + */ +export interface GuacamoleConfig { + colorDepth?: number; + width?: number; + height?: number; + dpi?: number; + resizeMethod?: string; + forceLossless?: boolean; + disableAudio?: boolean; + enableAudioInput?: boolean; + enableWallpaper?: boolean; + enableTheming?: boolean; + enableFontSmoothing?: boolean; + enableFullWindowDrag?: boolean; + enableDesktopComposition?: boolean; + enableMenuAnimations?: boolean; + disableBitmapCaching?: boolean; + disableOffscreenCaching?: boolean; + disableGlyphCaching?: boolean; + disableGfx?: boolean; + enablePrinting?: boolean; + printerName?: string; + enableDrive?: boolean; + driveName?: string; + drivePath?: string; + createDrivePath?: boolean; + disableDownload?: boolean; + disableUpload?: boolean; + enableTouch?: boolean; + clientName?: string; + console?: boolean; + initialProgram?: string; + serverLayout?: string; + timezone?: string; + gatewayHostname?: string; + gatewayPort?: number; + gatewayUsername?: string; + gatewayPassword?: string; + gatewayDomain?: string; + remoteApp?: string; + remoteAppDir?: string; + remoteAppArgs?: string; + normalizeClipboard?: string; + disableCopy?: boolean; + disablePaste?: boolean; + cursor?: string; + swapRedBlue?: boolean; + readOnly?: boolean; + recordingPath?: string; + recordingName?: string; + createRecordingPath?: boolean; + recordingExcludeOutput?: boolean; + recordingExcludeMouse?: boolean; + recordingIncludeKeys?: boolean; + wolSendPacket?: boolean; + wolMacAddr?: string; + wolBroadcastAddr?: string; + wolUdpPort?: number; + wolWaitTime?: number; +} diff --git a/src/types/homepage-types.ts b/src/types/homepage-types.ts index b661187..bbd2288 100644 --- a/src/types/homepage-types.ts +++ b/src/types/homepage-types.ts @@ -35,7 +35,9 @@ export type WidgetTypeId = | "service_grid" | "dashboard_links" | "search_links" - | "link_tree"; + | "link_tree" + | "docker_activity" + | "ssh_quick_connect"; export interface HomepageItemRow { id: number; @@ -108,13 +110,7 @@ export interface NotesConfig { } export type HostMetricKey = - | "cpu" - | "memory" - | "disk" - | "uptime" - | "network" - | "system" - | "processes"; + "cpu" | "memory" | "disk" | "uptime" | "network" | "system" | "processes"; export interface HostStatusConfig { hostId: number; @@ -155,11 +151,7 @@ export interface RssFeedConfig { // ---- New widget configs ---- export type MetricsChartMetric = - | "cpu" - | "memory" - | "disk" - | "net_rx" - | "net_tx"; + "cpu" | "memory" | "disk" | "net_rx" | "net_tx"; export type MetricsChartRange = "15m" | "1h" | "6h" | "24h"; export interface MetricsChartConfig { @@ -192,13 +184,7 @@ export interface PingStatusConfig { } export type ActivityType = - | "terminal" - | "file_manager" - | "docker" - | "tunnel" - | "rdp" - | "vnc" - | "telnet"; + "terminal" | "file_manager" | "docker" | "tunnel" | "rdp" | "vnc" | "telnet"; export interface RecentActivityConfig { maxItems: number; @@ -206,6 +192,18 @@ export interface RecentActivityConfig { showTimestamp: boolean; } +export interface DockerActivityConfig { + maxItems: number; + showHostName: boolean; +} + +export interface SshQuickConnectConfig { + hostIds: number[]; + connectionType: QuickConnectType; + showStatus: boolean; + layout: "grid" | "list"; +} + export interface TermixUptimeConfig { showDetailed: boolean; } diff --git a/src/types/host-sidebar-preferences.ts b/src/types/host-sidebar-preferences.ts new file mode 100644 index 0000000..b9601e7 --- /dev/null +++ b/src/types/host-sidebar-preferences.ts @@ -0,0 +1,213 @@ +/** + * Host sidebar preferences model. Shared by the frontend sidebar and the + * backend preferences endpoint (no framework imports, mirrors + * ./host-metrics.ts's dependency-free convention). Replaces the ~10 + * independent localStorage keys/custom events the sidebar used to manage + * individually. + * + * SortKey and StatusColorScheme are duplicated here (rather than imported + * from src/ui/sidebar/host-sort.ts / src/ui/hooks/use-status-color-scheme.ts) + * because those files use the "@/" frontend path alias, which the backend's + * NodeNext build cannot resolve. Keep the values below in sync with those + * two files. + */ + +export const HOST_SIDEBAR_PREFS_VERSION = 1; + +export type SortKey = + | "default" + | "name-asc" + | "name-desc" + | "ip-asc" + | "ip-desc" + | "status-online" + | "status-offline" + | "manual"; + +export type StatusColorScheme = "accent" | "status"; + +export type HostGroupKey = "folder" | "tag" | "status" | "protocol" | "auth"; + +export type HostDensity = "comfortable" | "compact"; + +export type HostTrayTrigger = "always" | "hover" | "click" | "actionsOnly"; + +export interface HostSidebarFilterState { + status: ("online" | "offline" | "pinned")[]; + authType: ("password" | "key" | "credential" | "none" | "opkssh")[]; + protocol: ("ssh" | "rdp" | "vnc" | "telnet")[]; + features: ("terminal" | "fileManager" | "tunnel" | "docker")[]; + tags: string[]; +} + +export interface HostSidebarDisplayPreferences { + density: HostDensity; + showTags: boolean; + trayTrigger: HostTrayTrigger; + statusColorScheme: StatusColorScheme; + /** When true, a host row needs a double click to launch its session. */ + openOnDoubleClick: boolean; +} + +export interface HostSidebarPreferences { + version: number; + sort: { key: SortKey; pinnedFirst: boolean }; + groupKey: HostGroupKey; + filters: HostSidebarFilterState; + openFolders: string[]; + display: HostSidebarDisplayPreferences; +} + +const GROUP_KEYS: HostGroupKey[] = [ + "folder", + "tag", + "status", + "protocol", + "auth", +]; +const SORT_KEYS: SortKey[] = [ + "default", + "name-asc", + "name-desc", + "ip-asc", + "ip-desc", + "status-online", + "status-offline", + "manual", +]; +const DENSITIES: HostDensity[] = ["comfortable", "compact"]; +const TRAY_TRIGGERS: HostTrayTrigger[] = [ + "always", + "hover", + "click", + "actionsOnly", +]; +const STATUS_COLOR_SCHEMES: StatusColorScheme[] = ["accent", "status"]; +const FILTER_STATUS: HostSidebarFilterState["status"] = [ + "online", + "offline", + "pinned", +]; +const FILTER_AUTH_TYPE: HostSidebarFilterState["authType"] = [ + "password", + "key", + "credential", + "none", + "opkssh", +]; +const FILTER_PROTOCOL: HostSidebarFilterState["protocol"] = [ + "ssh", + "rdp", + "vnc", + "telnet", +]; +const FILTER_FEATURES: HostSidebarFilterState["features"] = [ + "terminal", + "fileManager", + "tunnel", + "docker", +]; + +export function defaultHostSidebarPreferences(): HostSidebarPreferences { + return { + version: HOST_SIDEBAR_PREFS_VERSION, + sort: { key: "default", pinnedFirst: false }, + groupKey: "folder", + filters: { + status: [], + authType: [], + protocol: [], + features: [], + tags: [], + }, + openFolders: [], + display: { + density: "comfortable", + showTags: true, + trayTrigger: "always", + statusColorScheme: "accent", + openOnDoubleClick: false, + }, + }; +} + +function sanitizeStringArray(input: unknown): string[] { + if (!Array.isArray(input)) return []; + return input.filter((v): v is string => typeof v === "string"); +} + +function sanitizeEnumArray( + input: unknown, + allowed: readonly T[], +): T[] { + if (!Array.isArray(input)) return []; + return input.filter((v): v is T => allowed.includes(v as T)); +} + +export function sanitizeHostSidebarPreferences( + input: unknown, +): HostSidebarPreferences { + const defaults = defaultHostSidebarPreferences(); + if (!input || typeof input !== "object") return defaults; + const obj = input as Record; + + const sortObj = (obj.sort ?? {}) as Record; + const sort = { + key: SORT_KEYS.includes(sortObj.key as SortKey) + ? (sortObj.key as SortKey) + : defaults.sort.key, + pinnedFirst: + typeof sortObj.pinnedFirst === "boolean" + ? sortObj.pinnedFirst + : defaults.sort.pinnedFirst, + }; + + const groupKey = GROUP_KEYS.includes(obj.groupKey as HostGroupKey) + ? (obj.groupKey as HostGroupKey) + : defaults.groupKey; + + const filtersObj = (obj.filters ?? {}) as Record; + const filters: HostSidebarFilterState = { + status: sanitizeEnumArray(filtersObj.status, FILTER_STATUS), + authType: sanitizeEnumArray(filtersObj.authType, FILTER_AUTH_TYPE), + protocol: sanitizeEnumArray(filtersObj.protocol, FILTER_PROTOCOL), + features: sanitizeEnumArray(filtersObj.features, FILTER_FEATURES), + tags: sanitizeStringArray(filtersObj.tags), + }; + + const openFolders = sanitizeStringArray(obj.openFolders); + + const displayObj = (obj.display ?? {}) as Record; + const display: HostSidebarDisplayPreferences = { + density: DENSITIES.includes(displayObj.density as HostDensity) + ? (displayObj.density as HostDensity) + : defaults.display.density, + showTags: + typeof displayObj.showTags === "boolean" + ? displayObj.showTags + : defaults.display.showTags, + trayTrigger: TRAY_TRIGGERS.includes( + displayObj.trayTrigger as HostTrayTrigger, + ) + ? (displayObj.trayTrigger as HostTrayTrigger) + : defaults.display.trayTrigger, + statusColorScheme: STATUS_COLOR_SCHEMES.includes( + displayObj.statusColorScheme as StatusColorScheme, + ) + ? (displayObj.statusColorScheme as StatusColorScheme) + : defaults.display.statusColorScheme, + openOnDoubleClick: + typeof displayObj.openOnDoubleClick === "boolean" + ? displayObj.openOnDoubleClick + : defaults.display.openOnDoubleClick, + }; + + return { + version: HOST_SIDEBAR_PREFS_VERSION, + sort, + groupKey, + filters, + openFolders, + display, + }; +} diff --git a/src/types/index.ts b/src/types/index.ts index 3b2b9a0..44da38f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,15 @@ +import type { GuacamoleConfig } from "./guacamole-config.js"; +import type { StatsConfig } from "./stats-widgets.js"; import type { Client } from "ssh2"; import type { Request } from "express"; import type { RefObject } from "react"; +import type { HostAuthOverrides } from "./auth-protocols.js"; + +export type { + AuthOverrideProtocol, + HostAuthOverrideState, + HostAuthOverrides, +} from "./auth-protocols.js"; // ============================================================================ // SSO / AUTHENTICATION PROVIDER TYPES @@ -59,15 +68,16 @@ export interface LDAPProviderConfig { export type ConnectionType = "ssh" | "rdp" | "vnc" | "telnet"; export type SSHAuthType = - | "password" - | "key" - | "credential" - | "none" - | "opkssh" - | "tailscale"; + "password" | "key" | "credential" | "none" | "opkssh" | "tailscale"; export type GuacamoleAuthType = "password" | "credential"; +export interface ProxmoxStatsConfig { + nodeName?: string | null; + pollInterval?: number; + enabledCards?: string[]; +} + export interface ProxmoxConfig { defaultCredentialId: number | null; defaultAuthType?: string; @@ -95,6 +105,7 @@ export interface HostFeatureFlags { enableFileManager: boolean; // SSH only enableDocker: boolean; // SSH only enableTmuxMonitor: boolean; // SSH only + enableTerminalToolbar: boolean; // SSH only enableRemoteDesktop: boolean; // RDP, VNC only } @@ -107,7 +118,7 @@ export interface QuickAction { snippetId: number; } -export interface Host { +export type Host = { id: number; name: string; ip: string; @@ -126,6 +137,7 @@ export interface Host { | "agent" | "vault"; useWarpgate?: boolean; + shareSshAuth?: boolean; password?: string; key?: string; keyPassword?: string; @@ -151,7 +163,11 @@ export interface Host { enableDocker: boolean; enableProxmox: boolean; enableTmuxMonitor: boolean; + enableTerminalToolbar: boolean; + allowSessionSharing?: boolean; proxmoxConfig?: ProxmoxConfig | null; + enableProxmoxStats: boolean; + proxmoxStatsConfig?: ProxmoxStatsConfig | null; showTerminalInSidebar: boolean; showFileManagerInSidebar: boolean; showTunnelInSidebar: boolean; @@ -161,8 +177,8 @@ export interface Host { tunnelConnections: TunnelConnection[]; jumpHosts?: JumpHost[]; quickActions?: QuickAction[]; - statsConfig?: string | Record; - terminalConfig?: TerminalConfig; + statsConfig?: string | StatsConfig; + terminalConfig?: Partial; notes?: string; useSocks5?: boolean; @@ -184,7 +200,7 @@ export interface Host { domain?: string; security?: string; ignoreCert?: boolean; - guacamoleConfig?: string | Record; + guacamoleConfig?: string | GuacamoleConfig; dockerConfig?: Record | null; enableSsh?: boolean; @@ -207,20 +223,41 @@ export interface Host { telnetUser?: string; telnetPassword?: string; telnetCredentialId?: number | null; - rdpAuthType?: "direct" | "credential" | null; + rdpAuthType?: "direct" | "credential" | "none" | null; vncAuthType?: "direct" | "credential" | null; telnetAuthType?: "direct" | "credential" | null; + /** + * Stable identity across a desktop/server sync pair. `id` is an + * autoincrement local to whichever database produced the row, so it cannot + * name the same host on both sides; this can. Absent on hosts that have + * never been part of a sync. + */ + syncId?: string | null; createdAt: string; updatedAt: string; + sortOrder?: number | null; + connectionOrigin?: "local" | "remote" | null; + + /** Assigned when a host is opened in a tab; distinguishes duplicate tabs. */ + instanceId?: string; + hasKey?: boolean; hasKeyPassword?: boolean; + // Set by formatHostOutput() alongside hasKey/hasKeyPassword so the UI can + // tell a stored secret from an empty one without receiving it. + hasPassword?: boolean; + hasSudoPassword?: boolean; + hasRdpPassword?: boolean; + hasVncPassword?: boolean; + hasTelnetPassword?: boolean; isShared?: boolean; + authOverrides?: HostAuthOverrides; permissionLevel?: "connect" | "view" | "edit" | "manage"; sharedExpiresAt?: string; ownerUsername?: string; -} +}; export interface JumpHostData { hostId: number; @@ -234,7 +271,13 @@ export interface QuickActionData { export interface ProxyNode { host: string; port: number; - type: 4 | 5 | "http"; + /** + * The host editor writes "socks4"/"socks5"/"http", while proxy-helper.ts + * tests for "http" and casts everything else to 4|5 before handing it to the + * socks client. The two spellings have never agreed; typed as the union of + * what is actually stored rather than pretending one side is right. + */ + type: 4 | 5 | "http" | "socks4" | "socks5"; username?: string; password?: string; } @@ -245,6 +288,8 @@ export interface HostData { port: number; username: string; folder?: string; + /** Sub-host nesting: mutually exclusive with folder. */ + parentHostId?: number | string | null; tags?: string[]; pin?: boolean; authType: @@ -254,14 +299,18 @@ export interface HostData { | "none" | "opkssh" | "tailscale" - | "agent"; + | "agent" + | "vault"; useWarpgate?: boolean; + shareSshAuth?: boolean; password?: string; key?: File | string | null; keyPassword?: string; keyType?: string; sudoPassword?: string; credentialId?: number | null; + vaultProfileId?: number | null; + connectionOrigin?: "local" | "remote" | null; overrideCredentialUsername?: boolean; enableTerminal?: boolean; enableSessionLogging?: boolean; @@ -272,7 +321,11 @@ export interface HostData { enableDocker?: boolean; enableProxmox?: boolean; enableTmuxMonitor?: boolean; + enableTerminalToolbar?: boolean; + allowSessionSharing?: boolean; proxmoxConfig?: ProxmoxConfig | Record | null; + enableProxmoxStats?: boolean; + proxmoxStatsConfig?: ProxmoxStatsConfig | Record | null; showTerminalInSidebar?: boolean; showFileManagerInSidebar?: boolean; showTunnelInSidebar?: boolean; @@ -283,8 +336,8 @@ export interface HostData { tunnelConnections?: TunnelConnection[]; jumpHosts?: JumpHostData[]; quickActions?: QuickActionData[]; - statsConfig?: string | Record; - terminalConfig?: TerminalConfig; + statsConfig?: string | StatsConfig; + terminalConfig?: Partial; notes?: string; useSocks5?: boolean; @@ -306,7 +359,7 @@ export interface HostData { domain?: string; security?: string; ignoreCert?: boolean; - guacamoleConfig?: Record | null; + guacamoleConfig?: GuacamoleConfig | null; dockerConfig?: Record | null; enableSsh?: boolean; @@ -329,7 +382,7 @@ export interface HostData { telnetUser?: string; telnetPassword?: string; telnetCredentialId?: number | null; - rdpAuthType?: "direct" | "credential" | null; + rdpAuthType?: "direct" | "credential" | "none" | null; vncAuthType?: "direct" | "credential" | null; telnetAuthType?: "direct" | "credential" | null; } @@ -343,6 +396,8 @@ export interface SSHFolder { name: string; color?: string; icon?: string; + credentialId?: number | null; + sortOrder?: number | null; createdAt: string; updatedAt: string; } @@ -617,6 +672,7 @@ export interface TermixAlert { // ============================================================================ export interface TerminalConfig { + localEcho?: "default" | "off" | "auto" | "on"; cursorBlink: boolean; cursorStyle: "block" | "underline" | "bar"; fontSize: number; @@ -628,6 +684,7 @@ export interface TerminalConfig { scrollback: number; bellStyle: "none" | "sound" | "visual" | "both"; rightClickSelectsWord: boolean; + macOptionIsMeta: boolean; fastScrollModifier: "alt" | "ctrl" | "shift"; fastScrollSensitivity: number; minimumContrastRatio: number; @@ -639,6 +696,7 @@ export interface TerminalConfig { autoMosh: boolean; moshCommand: string; sudoPasswordAutoFill: boolean; + sudoPassword?: string | null; keepaliveInterval?: number; keepaliveCountMax?: number; autoTmux: boolean; @@ -660,9 +718,10 @@ export interface TerminalConfig { customThemeColors?: { background: string; foreground: string; - cursor: string; - cursorAccent: string; - selectionBackground: string; + cursor?: string; + cursorAccent?: string; + selectionBackground?: string; + selectionForeground?: string; black: string; red: string; green: string; @@ -698,6 +757,7 @@ export interface TabContextTab { | "file_manager" | "user_profile" | "docker" + | "tunnel" | "network_graph" | "tmux_monitor" // --- tmux-monitor --- | "rdp" @@ -717,6 +777,7 @@ export interface TerminalRefHandle { isConnected?: () => boolean; fit?: () => void; sendInput?: (data: string) => void; + subscribeOutput?: (listener: (data: string) => void) => () => void; notifyResize?: () => void; refresh?: () => void; openFileManager?: () => void; @@ -768,12 +829,7 @@ export type ErrorType = // ============================================================================ export type AuthType = - | "password" - | "key" - | "credential" - | "none" - | "opkssh" - | "tailscale"; + "password" | "key" | "credential" | "none" | "opkssh" | "tailscale"; export type KeyType = "rsa" | "ecdsa" | "ed25519"; @@ -902,39 +958,8 @@ export interface FolderStats { }>; } -// ============================================================================ -// SNIPPETS TYPES -// ============================================================================ - -export interface Snippet { - id: number; - userId: string; - name: string; - content: string; - description?: string; - folder?: string; - order?: number; - createdAt: string; - updatedAt: string; -} - -export interface SnippetData { - name: string; - content: string; - description?: string; - folder?: string; - order?: number; -} - -export interface SnippetFolder { - id: number; - userId: string; - name: string; - color?: string; - icon?: string; - createdAt: string; - updatedAt: string; -} +// Snippet, SnippetFolder types live in ui-types.ts (the shape actually used +// by SnippetsPanel.tsx); this file's older definitions were unused and removed. // ============================================================================ // BACKEND TYPES diff --git a/src/types/keybindings.ts b/src/types/keybindings.ts new file mode 100644 index 0000000..5286560 --- /dev/null +++ b/src/types/keybindings.ts @@ -0,0 +1,29 @@ +export interface KeyCombo { + key: string; + isCode: boolean; + ctrl: boolean; + alt: boolean; + shift: boolean; + meta: boolean; +} + +export type KeybindingActionType = + "copy" | "paste" | "sendControlCode" | "sendText" | "runSnippet"; + +export interface KeybindingAction { + type: KeybindingActionType; + text?: string; + controlCode?: string; + snippetId?: string; + appendEnter?: boolean; +} + +export interface CustomKeybinding { + id: string; + combo: KeyCombo; + action: KeybindingAction; + enabled: boolean; + overridesDefaultId?: string; + createdAt: string; + updatedAt: string; +} diff --git a/src/types/proxmox.ts b/src/types/proxmox.ts index e7507e3..ba7b0b4 100644 --- a/src/types/proxmox.ts +++ b/src/types/proxmox.ts @@ -13,6 +13,7 @@ export interface ProxmoxDiscoverResult { guests: ProxmoxGuest[]; credentialId: number | null; defaultCredentialId: number | null; + jumpHosts?: unknown[] | null; } export interface ProxmoxSyncResult { @@ -22,3 +23,102 @@ export interface ProxmoxSyncResult { skipped: number; errors: string[]; } + +// Frontend-side mirror of the backend Proxmox Stats collectors' return shapes +// (src/backend/hosts/metrics/proxmox/*). Kept in sync by hand, same convention +// as ServerMetrics in main-axios.ts mirroring collectMetrics. + +export interface ProxmoxNodeStats { + cpu: { + percent: number | null; + cores: number | null; + load: [number, number, number] | null; + }; + memory: { + percent: number | null; + usedGiB: number | null; + totalGiB: number | null; + }; + disk: { + percent: number | null; + usedGiB: number | null; + totalGiB: number | null; + }; + uptime: { + seconds: number | null; + formatted: string | null; + }; + system: { + hostname: string | null; + kernel: string | null; + pveVersion: string | null; + }; +} + +export interface ProxmoxNodeNetwork { + interfaces: Array<{ + name: string; + ip: string | null; + state: string | null; + rxBytes: string | null; + txBytes: string | null; + }>; +} + +export interface ProxmoxGuestSummary { + vmid: number; + name: string; + type: "qemu" | "lxc"; + status: string; + cpuPercent: number | null; + memPercent: number | null; + memUsedGiB: number | null; + memTotalGiB: number | null; + diskPercent: number | null; + diskUsedGiB: number | null; + diskTotalGiB: number | null; + uptimeSeconds: number | null; +} + +export interface ProxmoxGuestsSummary { + guests: ProxmoxGuestSummary[]; + counts: { running: number; stopped: number; total: number }; +} + +export interface ProxmoxStoragePool { + name: string; + type: string; + active: boolean; + enabled: boolean; + usedGiB: number | null; + totalGiB: number | null; + availGiB: number | null; + percent: number | null; +} + +export interface ProxmoxStorage { + pools: ProxmoxStoragePool[]; +} + +export type ProxmoxClusterHealth = + | { clustered: false } + | { + clustered: true; + quorate: boolean; + clusterName: string | null; + nodes: Array<{ + name: string; + online: boolean; + local: boolean; + ip: string | null; + }>; + }; + +export interface ProxmoxStatsSnapshot { + node: ProxmoxNodeStats; + network: ProxmoxNodeNetwork; + guests: ProxmoxGuestsSummary; + storage: ProxmoxStorage; + cluster: ProxmoxClusterHealth; + lastChecked: string; +} diff --git a/src/types/stats-widgets.ts b/src/types/stats-widgets.ts index 33b485c..165b820 100644 --- a/src/types/stats-widgets.ts +++ b/src/types/stats-widgets.ts @@ -70,6 +70,10 @@ export interface StatsConfig { metricsInterval: number; useGlobalMetricsInterval?: boolean; disableTcpPing?: boolean; + /** Filesystem mount points to leave out of the disk widget. */ + excludedMounts?: string[]; + /** Extra paths to monitor, including paths inside bind-mounted containers. */ + monitoredMounts?: Array<{ path: string; label?: string }>; } export const DEFAULT_STATS_CONFIG: StatsConfig = { diff --git a/src/types/touch-input-settings.ts b/src/types/touch-input-settings.ts new file mode 100644 index 0000000..bd98b0c --- /dev/null +++ b/src/types/touch-input-settings.ts @@ -0,0 +1,127 @@ +export const TOUCH_INPUT_SETTING_KEY = "touch_input_settings"; + +export interface TouchInputSettings { + enabled: boolean; + momentumEnabled: boolean; + dragThresholdPx: number; + maxWheelDeltaPx: number; + momentumSampleWindowMs: number; + releaseGracePeriodMs: number; + minimumVelocityPxPerMs: number; + maximumVelocityPxPerMs: number; + maximumDurationMs: number; + maximumTravelPx: number; + decayTimeMs: number; + pixelsPerTick: number; + maximumFrameIntervalMs: number; + maximumTicksPerFrame: number; +} + +export const TOUCH_INPUT_DEFAULTS: TouchInputSettings = { + enabled: true, + momentumEnabled: true, + dragThresholdPx: 6, + maxWheelDeltaPx: 120, + momentumSampleWindowMs: 100, + releaseGracePeriodMs: 120, + minimumVelocityPxPerMs: 0.15, + maximumVelocityPxPerMs: 2.5, + maximumDurationMs: 1_000, + maximumTravelPx: 720, + decayTimeMs: 300, + pixelsPerTick: 12, + maximumFrameIntervalMs: 32, + maximumTicksPerFrame: 4, +}; + +export type TouchInputNumericKey = Exclude< + keyof TouchInputSettings, + "enabled" | "momentumEnabled" +>; + +export const TOUCH_INPUT_NUMERIC_BOUNDS: Record< + TouchInputNumericKey, + { min: number; max: number; step: number } +> = { + dragThresholdPx: { min: 0, max: 100, step: 1 }, + maxWheelDeltaPx: { min: 1, max: 1_000, step: 1 }, + momentumSampleWindowMs: { min: 10, max: 1_000, step: 10 }, + releaseGracePeriodMs: { min: 0, max: 2_000, step: 10 }, + minimumVelocityPxPerMs: { min: 0, max: 10, step: 0.01 }, + maximumVelocityPxPerMs: { min: 0.01, max: 20, step: 0.01 }, + maximumDurationMs: { min: 0, max: 10_000, step: 100 }, + maximumTravelPx: { min: 0, max: 10_000, step: 10 }, + decayTimeMs: { min: 1, max: 5_000, step: 10 }, + pixelsPerTick: { min: 1, max: 500, step: 1 }, + maximumFrameIntervalMs: { min: 1, max: 1_000, step: 1 }, + maximumTicksPerFrame: { min: 1, max: 100, step: 1 }, +}; + +export function normalizeTouchInputSettings( + value: unknown, +): TouchInputSettings { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return { ...TOUCH_INPUT_DEFAULTS }; + } + + const input = value as Record; + const normalized: TouchInputSettings = { ...TOUCH_INPUT_DEFAULTS }; + for (const key of ["enabled", "momentumEnabled"] as const) { + if (typeof input[key] === "boolean") normalized[key] = input[key]; + } + for (const [key, bounds] of Object.entries(TOUCH_INPUT_NUMERIC_BOUNDS) as [ + TouchInputNumericKey, + { min: number; max: number }, + ][]) { + const candidate = input[key]; + if ( + typeof candidate === "number" && + Number.isFinite(candidate) && + candidate >= bounds.min && + candidate <= bounds.max + ) { + normalized[key] = candidate; + } + } + return normalized; +} + +export function validateTouchInputSettingsUpdate( + value: unknown, +): string | null { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return "settings must be an object"; + } + const input = value as Record; + const validKeys = new Set(Object.keys(TOUCH_INPUT_DEFAULTS)); + const unknownKey = Object.keys(input).find((key) => !validKeys.has(key)); + if (unknownKey) return `Unknown touch input setting: ${unknownKey}`; + for (const key of ["enabled", "momentumEnabled"] as const) { + if (key in input && typeof input[key] !== "boolean") { + return `${key} must be a boolean`; + } + } + for (const [key, bounds] of Object.entries(TOUCH_INPUT_NUMERIC_BOUNDS) as [ + TouchInputNumericKey, + { min: number; max: number }, + ][]) { + if (!(key in input)) continue; + const candidate = input[key]; + if ( + typeof candidate !== "number" || + !Number.isFinite(candidate) || + candidate < bounds.min || + candidate > bounds.max + ) { + return `${key} must be a number between ${bounds.min} and ${bounds.max}`; + } + } + if ( + typeof input.minimumVelocityPxPerMs === "number" && + typeof input.maximumVelocityPxPerMs === "number" && + input.minimumVelocityPxPerMs > input.maximumVelocityPxPerMs + ) { + return "minimumVelocityPxPerMs must not exceed maximumVelocityPxPerMs"; + } + return null; +} diff --git a/src/types/ui-preferences.ts b/src/types/ui-preferences.ts new file mode 100644 index 0000000..1777775 --- /dev/null +++ b/src/types/ui-preferences.ts @@ -0,0 +1,499 @@ +/** + * App-wide UI complexity preferences. Shared by the frontend UI preferences + * context and the backend preferences endpoint (no framework imports, mirrors + * ./host-sidebar-preferences.ts's dependency-free convention -- the backend's + * NodeNext build cannot resolve the "@/" frontend path alias). + * + * The model stores the user's *intent* -- a preset plus the individual knobs + * they have deliberately changed -- not a second copy of values other stores + * already own. Areas whose knobs already live somewhere else (host sidebar + * blob, user_preferences.hiddenRailTabs, a handful of localStorage keys) are + * seeded from the preset when it changes; reads keep going to the existing + * store. See applyPresetSideEffects on the frontend. + * + * "balanced" is exactly today's behavior. Every value in PRESETS.balanced is + * transcribed from the defaults that were already in the code, so existing + * users who land on it see no change at all. + */ + +export const UI_PREFERENCES_VERSION = 1; + +/** Bump when onboarding gains steps existing users should be shown again. */ +export const UI_ONBOARDING_VERSION = 2; + +export type UiPreset = "simple" | "balanced" | "advanced" | "custom"; + +export type UiAreaKey = + | "chrome" + | "hostList" + | "credentialList" + | "rail" + | "dashboard" + | "terminal" + | "fileManager" + | "docker" + | "hostMetrics" + | "hostEditor" + | "homepage"; + +export type UiDensity = "comfortable" | "compact"; +export type UiTrayTrigger = "always" | "hover" | "click" | "actionsOnly"; +export type UiRowActions = "essential" | "full"; +export type UiEmptyStateVerbosity = "minimal" | "guided"; +export type UiToolbarDensity = "icon" | "labeled" | "expanded"; +export type UiFileViewMode = "grid" | "list"; +export type UiDockerViewMode = "list" | "detail"; +export type UiHostEditorMode = "simple" | "full"; + +export interface UiChromePreferences { + showBreadcrumbs: boolean; + showStatusBar: boolean; + emptyStateVerbosity: UiEmptyStateVerbosity; +} + +export interface UiHostListPreferences { + density: UiDensity; + showTags: boolean; + showResourceBars: boolean; + showStatusStripes: boolean; + trayTrigger: UiTrayTrigger; + rowActions: UiRowActions; +} + +export interface UiCredentialListPreferences { + density: UiDensity; + showTags: boolean; +} + +export interface UiRailPreferences { + hiddenTabs: string[]; +} + +export interface UiDashboardPreferences { + enabledCards: string[]; +} + +export interface UiTerminalPreferences { + toolbarDensity: UiToolbarDensity; +} + +export interface UiFileManagerPreferences { + viewMode: UiFileViewMode; + showHiddenFiles: boolean; +} + +export interface UiDockerPreferences { + viewMode: UiDockerViewMode; +} + +export interface UiHostMetricsPreferences { + enabledCards: string[]; + columns: number; +} + +export interface UiHostEditorPreferences { + mode: UiHostEditorMode; +} + +export interface UiHomepagePreferences { + /** null means "never preset-driven" -- a preset must not touch the canvas. */ + enabledWidgets: string[] | null; +} + +export interface UiAreaPreferences { + chrome: UiChromePreferences; + hostList: UiHostListPreferences; + credentialList: UiCredentialListPreferences; + rail: UiRailPreferences; + dashboard: UiDashboardPreferences; + terminal: UiTerminalPreferences; + fileManager: UiFileManagerPreferences; + docker: UiDockerPreferences; + hostMetrics: UiHostMetricsPreferences; + hostEditor: UiHostEditorPreferences; + homepage: UiHomepagePreferences; +} + +export type UiOverrides = { + [A in UiAreaKey]?: Partial; +}; + +export interface UiOnboardingState { + /** 0 means "never completed". Compared against UI_ONBOARDING_VERSION. */ + completedVersion: number; + completedAt: string | null; + skipped: boolean; +} + +export interface UiPreferences { + version: number; + preset: UiPreset; + overrides: UiOverrides; + onboarding: UiOnboardingState; +} + +const PRESET_VALUES: UiPreset[] = ["simple", "balanced", "advanced", "custom"]; + +/** + * Rail views Simple keeps. Cutting all the way down to hosts+credentials makes + * the app feel broken, so connections and snippets stay: snippets is the most + * approachable power feature and connections is where troubleshooting starts. + */ +const SIMPLE_RAIL_VISIBLE = ["hosts", "credentials", "connections", "snippets"]; + +/** Every hideable rail view, mirroring HideableRailView in sidebar/AppRail.tsx. */ +const ALL_HIDEABLE_RAIL_VIEWS = [ + "hosts", + "credentials", + "termix-id", + "quick-connect", + "serial", + "ssh-tools", + "snippets", + "macros", + "history", + "split-screen", + "connections", + "session-logs", + "alerts", + "fleets", + "workspaces", + "network_graph", + "homepage", + "ai", +]; + +const SIMPLE_HIDDEN_RAIL_TABS = ALL_HIDEABLE_RAIL_VIEWS.filter( + (view) => !SIMPLE_RAIL_VISIBLE.includes(view), +); + +/** Dashboard card ids, mirroring DASHBOARD_CARDS in ui/lib/theme.ts. */ +const BALANCED_DASHBOARD_CARDS = [ + "stats_bar", + "counters_bar", + "quick_actions", + "host_status", + "recent_activity", +]; +// Advanced adds service links but leaves network_graph and homepage_preview +// off: both are wide, and enabling them by default pushes the dashboard past +// the edge of the screen. They stay available in the Add card tray. +const ADVANCED_DASHBOARD_CARDS = [...BALANCED_DASHBOARD_CARDS, "service_links"]; + +/** Host metrics card ids, mirroring CARD_DEFINITIONS in features/host-metrics/cards. */ +const SIMPLE_HOST_METRICS_CARDS = ["cpu", "memory", "disk"]; +const BALANCED_HOST_METRICS_CARDS = [ + "cpu", + "memory", + "disk", + "network", + "uptime", + "system", +]; +const ADVANCED_HOST_METRICS_CARDS = [ + ...BALANCED_HOST_METRICS_CARDS, + "login_stats", + "ports", + "processes", + "firewall", + "temperature", +]; + +export const PRESETS: Record, UiAreaPreferences> = { + simple: { + chrome: { + showBreadcrumbs: false, + showStatusBar: false, + emptyStateVerbosity: "guided", + }, + hostList: { + density: "comfortable", + showTags: false, + showResourceBars: false, + showStatusStripes: false, + // "always" permanently renders the management row and resource bars, + // which is the wall of buttons the issue calls intimidating. + trayTrigger: "actionsOnly", + rowActions: "essential", + }, + credentialList: { density: "comfortable", showTags: false }, + rail: { hiddenTabs: SIMPLE_HIDDEN_RAIL_TABS }, + dashboard: { + enabledCards: [ + "stats_bar", + "counters_bar", + "quick_actions", + "host_status", + ], + }, + terminal: { toolbarDensity: "icon" }, + fileManager: { viewMode: "grid", showHiddenFiles: false }, + docker: { viewMode: "list" }, + hostMetrics: { enabledCards: SIMPLE_HOST_METRICS_CARDS, columns: 1 }, + hostEditor: { mode: "simple" }, + homepage: { enabledWidgets: null }, + }, + balanced: { + chrome: { + showBreadcrumbs: true, + showStatusBar: true, + emptyStateVerbosity: "minimal", + }, + hostList: { + density: "comfortable", + showTags: true, + showResourceBars: true, + showStatusStripes: true, + trayTrigger: "always", + rowActions: "full", + }, + credentialList: { density: "comfortable", showTags: true }, + rail: { hiddenTabs: [] }, + dashboard: { enabledCards: BALANCED_DASHBOARD_CARDS }, + terminal: { toolbarDensity: "labeled" }, + // FileManager.tsx has always defaulted to grid when nothing is stored. + fileManager: { viewMode: "grid", showHiddenFiles: false }, + docker: { viewMode: "list" }, + // 3 is defaultLayoutFromWidgets's own default, i.e. today's behavior. + hostMetrics: { enabledCards: BALANCED_HOST_METRICS_CARDS, columns: 3 }, + hostEditor: { mode: "full" }, + homepage: { enabledWidgets: null }, + }, + advanced: { + chrome: { + showBreadcrumbs: true, + showStatusBar: true, + emptyStateVerbosity: "minimal", + }, + hostList: { + density: "compact", + showTags: true, + showResourceBars: true, + showStatusStripes: true, + trayTrigger: "always", + rowActions: "full", + }, + credentialList: { density: "compact", showTags: true }, + rail: { hiddenTabs: [] }, + dashboard: { enabledCards: ADVANCED_DASHBOARD_CARDS }, + terminal: { toolbarDensity: "expanded" }, + // List packs more files and metadata per screen than the grid. + fileManager: { viewMode: "list", showHiddenFiles: true }, + docker: { viewMode: "detail" }, + hostMetrics: { enabledCards: ADVANCED_HOST_METRICS_CARDS, columns: 4 }, + hostEditor: { mode: "full" }, + homepage: { enabledWidgets: null }, + }, +}; + +type FieldSpec = + | { kind: "enum"; values: readonly string[] } + | { kind: "bool" } + | { kind: "int"; min: number; max: number } + | { kind: "stringArray" } + | { kind: "nullableStringArray" }; + +/** + * Field descriptors for every area knob. Unlike the flat sanitizers on the + * sidebar preference blobs, overrides are a sparse two-level map, so one table + * drives both levels instead of a ternary per field. + */ +const AREA_SPECS: { + [A in UiAreaKey]: Record; +} = { + chrome: { + showBreadcrumbs: { kind: "bool" }, + showStatusBar: { kind: "bool" }, + emptyStateVerbosity: { kind: "enum", values: ["minimal", "guided"] }, + }, + hostList: { + density: { kind: "enum", values: ["comfortable", "compact"] }, + showTags: { kind: "bool" }, + showResourceBars: { kind: "bool" }, + showStatusStripes: { kind: "bool" }, + trayTrigger: { + kind: "enum", + values: ["always", "hover", "click", "actionsOnly"], + }, + rowActions: { kind: "enum", values: ["essential", "full"] }, + }, + credentialList: { + density: { kind: "enum", values: ["comfortable", "compact"] }, + showTags: { kind: "bool" }, + }, + rail: { + hiddenTabs: { kind: "stringArray" }, + }, + dashboard: { + enabledCards: { kind: "stringArray" }, + }, + terminal: { + toolbarDensity: { + kind: "enum", + values: ["icon", "labeled", "expanded"], + }, + }, + fileManager: { + viewMode: { kind: "enum", values: ["grid", "list"] }, + showHiddenFiles: { kind: "bool" }, + }, + docker: { + viewMode: { kind: "enum", values: ["list", "detail"] }, + }, + hostMetrics: { + enabledCards: { kind: "stringArray" }, + columns: { kind: "int", min: 1, max: 4 }, + }, + hostEditor: { + mode: { kind: "enum", values: ["simple", "full"] }, + }, + homepage: { + enabledWidgets: { kind: "nullableStringArray" }, + }, +}; + +export const UI_AREA_KEYS = Object.keys(AREA_SPECS) as UiAreaKey[]; + +/** Returns undefined when the value fails its spec, so callers can drop it. */ +function coerce(spec: FieldSpec, value: unknown): unknown | undefined { + switch (spec.kind) { + case "bool": + return typeof value === "boolean" ? value : undefined; + case "enum": + return typeof value === "string" && spec.values.includes(value) + ? value + : undefined; + case "int": { + if (typeof value !== "number" || !Number.isFinite(value)) + return undefined; + const rounded = Math.round(value); + if (rounded < spec.min || rounded > spec.max) return undefined; + return rounded; + } + case "stringArray": + return Array.isArray(value) + ? value.filter((v): v is string => typeof v === "string") + : undefined; + case "nullableStringArray": + if (value === null) return null; + return Array.isArray(value) + ? value.filter((v): v is string => typeof v === "string") + : undefined; + } +} + +/** + * Drops unknown areas, unknown keys and invalid values, then prunes areas that + * ended up empty. The pruning matters: it keeps + * Object.keys(overrides).length > 0 an honest "has customizations" check for + * the settings UI rather than something that accumulates {hostList:{}} noise. + */ +export function sanitizeUiOverrides(input: unknown): UiOverrides { + const out: Record> = {}; + if (!input || typeof input !== "object") return out as UiOverrides; + + const specsByArea = AREA_SPECS as unknown as Record< + string, + Record + >; + + for (const [area, areaValue] of Object.entries( + input as Record, + )) { + const specs = specsByArea[area]; + if (!specs || !areaValue || typeof areaValue !== "object") continue; + + const bucket: Record = {}; + for (const [key, raw] of Object.entries( + areaValue as Record, + )) { + const spec = specs[key]; + if (!spec) continue; + const value = coerce(spec, raw); + if (value !== undefined) bucket[key] = value; + } + + if (Object.keys(bucket).length > 0) out[area] = bucket; + } + + return out as UiOverrides; +} + +function sanitizeOnboarding(input: unknown): UiOnboardingState { + const defaults: UiOnboardingState = { + completedVersion: 0, + completedAt: null, + skipped: false, + }; + if (!input || typeof input !== "object") return defaults; + const obj = input as Record; + + return { + completedVersion: + typeof obj.completedVersion === "number" && + Number.isFinite(obj.completedVersion) && + obj.completedVersion >= 0 + ? Math.round(obj.completedVersion) + : defaults.completedVersion, + completedAt: + typeof obj.completedAt === "string" + ? obj.completedAt + : defaults.completedAt, + skipped: typeof obj.skipped === "boolean" ? obj.skipped : defaults.skipped, + }; +} + +export function defaultUiPreferences(): UiPreferences { + return { + version: UI_PREFERENCES_VERSION, + preset: "balanced", + overrides: {}, + onboarding: { completedVersion: 0, completedAt: null, skipped: false }, + }; +} + +export function sanitizeUiPreferences(input: unknown): UiPreferences { + const defaults = defaultUiPreferences(); + if (!input || typeof input !== "object") return defaults; + const obj = input as Record; + + return { + version: UI_PREFERENCES_VERSION, + preset: PRESET_VALUES.includes(obj.preset as UiPreset) + ? (obj.preset as UiPreset) + : defaults.preset, + overrides: sanitizeUiOverrides(obj.overrides), + onboarding: sanitizeOnboarding(obj.onboarding), + }; +} + +/** + * Effective values for one area: preset defaults with the user's overrides + * layered on top. "custom" only labels a diverged state, so it re-bases on + * balanced rather than carrying a fourth value table. + */ +export function resolveArea( + preferences: UiPreferences, + area: A, +): UiAreaPreferences[A] { + const base = + PRESETS[preferences.preset === "custom" ? "balanced" : preferences.preset][ + area + ]; + const override = preferences.overrides[area]; + return override ? { ...base, ...override } : base; +} + +export function hasUiOverrides(preferences: UiPreferences): boolean { + return Object.keys(preferences.overrides).length > 0; +} + +/** + * What the settings UI should show as the active preset. "custom" is derived + * rather than stored, so clearing every override automatically restores the + * user's chosen preset instead of stranding them on a label. + */ +export function effectivePresetLabel(preferences: UiPreferences): UiPreset { + if (preferences.preset === "custom") return "custom"; + return hasUiOverrides(preferences) ? "custom" : preferences.preset; +} diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index 304d412..79708ca 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -1,3 +1,8 @@ +import type { GuacamoleConfig } from "./guacamole-config.js"; +import type { TerminalConfig } from "./index.js"; +import type { StatsConfig } from "./stats-widgets.js"; +import type { HostAuthOverrides } from "./auth-protocols.js"; + export type Host = { id: string; name: string; @@ -5,7 +10,17 @@ export type Host = { ip: string; port: number; folder: string; + /** Sub-host nesting: the id of the host this one is organized under, if any. */ + parentHostId?: string | null; + /** + * Sub-hosts nested under this host, populated client-side by buildHostTree. + * A host with children still renders and behaves as a normal, connectable + * HostItem row -- this only adds an expand/collapse chevron for its nested + * children, it never wraps the host in a synthetic folder node. + */ + childHosts?: Host[]; online: boolean; + status?: "online" | "reachable" | "offline" | "unknown"; cpu: number | null; ram: number | null; lastAccess: string; @@ -20,6 +35,7 @@ export type Host = { | "vault" | "agent"; useWarpgate?: boolean; + shareSshAuth?: boolean; credentialId?: string; vaultProfileId?: string; overrideCredentialUsername?: boolean; @@ -36,51 +52,31 @@ export type Host = { macAddress?: string; wolBroadcastAddress?: string; pin?: boolean; + sortOrder?: number | null; enableTerminal: boolean; enableCommandHistory: boolean; - terminalConfig?: { - cursorBlink: boolean; - cursorStyle: "block" | "underline" | "bar"; - fontSize: number; - fontFamily: string; - letterSpacing: number; - lineHeight: number; - theme: string; - scrollback: number; - bellStyle: "none" | "sound" | "visual" | "both"; - rightClickSelectsWord: boolean; - fastScrollModifier: "alt" | "ctrl" | "shift"; - fastScrollSensitivity: number; - minimumContrastRatio: number; - backspaceMode: "normal" | "control-h"; - agentForwarding: boolean; - autoMosh: boolean; - moshCommand: string; - autoTmux: boolean; - sudoPasswordAutoFill: boolean; - sudoPassword?: string; - keepaliveInterval?: number; - keepaliveCountMax?: number; - environmentVariables: { key: string; value: string }[]; - startupSnippetId?: number | null; - linkClickBehavior?: "confirm" | "direct"; - agentSocketPath?: string; - }; + enableSessionLogging?: boolean; + allowSessionSharing?: boolean; + /** Stable identity across a desktop/server sync pair. */ + syncId?: string | null; + terminalConfig?: Partial; useSocks5?: boolean; socks5Host?: string; socks5Port?: number; + connectionOrigin?: "local" | "remote" | null; socks5Username?: string; socks5Password?: string; socks5ProxyChain?: { host: string; port: number; - type: 4 | 5 | "http" | string; + type: 4 | 5 | "http" | "socks4" | "socks5"; username?: string; password?: string; }[]; - jumpHosts?: { hostId: string }[]; + /** hostid is a legacy lowercase spelling still present in stored rows. */ + jumpHosts?: { hostId: string; hostid?: string }[]; portKnockSequence?: { port: number; protocol: "tcp" | "udp"; @@ -110,7 +106,18 @@ export type Host = { } | null; enableProxmox: boolean; enableTmuxMonitor: boolean; + enableTerminalToolbar: boolean; proxmoxConfig?: { + source?: { + source: "proxmox"; + sourceHostId: number; + node: string; + vmid: number; + type: "qemu" | "lxc"; + lastSeenAt?: string; + lastStatus?: string; + missingSince?: string | null; + }; defaultCredentialId: number | null; defaultAuthType?: string; windowsPatterns: string; @@ -130,16 +137,14 @@ export type Host = { errors: string[]; }; } | null; + enableProxmoxStats: boolean; + proxmoxStatsConfig?: { + nodeName?: string | null; + pollInterval?: number; + enabledCards?: string[]; + } | null; - statsConfig?: { - statusCheckEnabled: boolean; - statusCheckInterval: number; - useGlobalStatusInterval: boolean; - metricsEnabled: boolean; - metricsInterval: number; - useGlobalMetricsInterval: boolean; - enabledWidgets: string[]; - }; + statsConfig?: StatsConfig; quickActions: { name: string; snippetId: string }[]; enableSsh: boolean; @@ -152,24 +157,32 @@ export type Host = { vncPort: number; telnetPort: number; + rdpAuthType?: "direct" | "credential" | "none"; rdpCredentialId?: string; rdpUser?: string; rdpPassword?: string; + hasRdpPassword?: boolean; domain?: string; security?: string; ignoreCert?: boolean; + vncAuthType?: "direct" | "credential"; vncCredentialId?: string; vncPassword?: string; + hasVncPassword?: boolean; vncUser?: string; + telnetAuthType?: "direct" | "credential"; + telnetCredentialId?: string; telnetUser?: string; telnetPassword?: string; + hasTelnetPassword?: boolean; - guacamoleConfig?: Record; + guacamoleConfig?: GuacamoleConfig; forceKeyboardInteractive?: boolean; isShared?: boolean; + authOverrides?: HostAuthOverrides; permissionLevel?: SharePermissionLevel; sharedExpiresAt?: string; ownerUsername?: string; @@ -189,6 +202,9 @@ export type Credential = { description?: string; folder?: string; tags?: string[]; + pin?: boolean; + sortOrder?: number | null; + certPublicKey?: string; }; // HashiCorp Vault SSH signer profile โ€” shareable connection settings only @@ -217,15 +233,19 @@ export type HostFolder = { path?: string; color?: string; icon?: string; + credentialId?: number | null; + sortOrder?: number | null; }; export type TabType = | "dashboard" | "terminal" + | "local-terminal" | "rdp" | "vnc" | "telnet" | "host-metrics" + | "proxmox-stats" | "files" | "host-manager" | "user-profile" @@ -235,7 +255,19 @@ export type TabType = | "network_graph" | "tmux_monitor" // --- tmux-monitor --- | "serial" - | "homepage"; + | "homepage" + | "fleet-inventory" + // Rail panels that can also open full-width in the main area. + | "termix-id" + | "alerts" + | "session-logs" + | "snippets" + | "macros" + | "history" + | "ssh-tools" + | "automations" + | "ai" + | "split-screen"; export type SerialConfig = { path: string; @@ -245,28 +277,6 @@ export type SerialConfig = { parity: "none" | "even" | "odd"; }; -export type TunnelStatusValue = - | "CONNECTED" - | "CONNECTING" - | "DISCONNECTING" - | "DISCONNECTED" - | "ERROR" - | "WAITING"; -export type TunnelMode = "local" | "remote" | "dynamic"; - -export type Tunnel = { - id: string; - hostId: string; - sourcePort: number; - endpointHost: string; - endpointPort: number; - status: TunnelStatusValue; - mode: TunnelMode; - reason?: string; - retryCount?: number; - maxRetries?: number; -}; - export type Tab = { id: string; instanceId: string; @@ -276,37 +286,35 @@ export type Tab = { host?: Host; openedAt: number; restoredSessionId?: string | null; + /** Set when this tab joins someone else's live shared session instead of connecting/attaching its own. */ + joinSharedSessionId?: string | null; + joinShareId?: string | null; initialFilePath?: string; + /** Directory to open a Files tab into, distinct from initialFilePath (a specific file to open in an editor window). */ + initialPath?: string; + /** Which fleet a fleet-inventory tab is currently showing (singleton tab, re-targeted on reopen). */ + fleetId?: number; serialConfig?: SerialConfig; + /** Present only on a split-screen container tab. Pane ids reference live child tabs. */ + splitConfig?: SplitTabConfig; + /** Hides this session from the top-level tab bar while it belongs to a split tab. */ + parentSplitTabId?: string; terminalRef?: import("react").RefObject<{ disconnect?: () => void; isConnected?: () => boolean; sendInput?: (data: string) => void; + subscribeOutput?: (listener: (data: string) => void) => () => void; + paste?: (text: string) => void; reconnect?: () => void; fit?: () => void; notifyResize?: () => void; + refresh?: () => void; getApplicationCursorKeysMode?: () => boolean; + openShareModal?: () => void; + canShare?: () => boolean; } | null>; }; -export type DockerContainerStatus = - | "running" - | "exited" - | "paused" - | "created" - | "restarting"; - -export type DockerContainer = { - id: string; - name: string; - image: string; - status: DockerContainerStatus; - cpu: number; - memory: string; - ports: string[]; - created: string; -}; - export type DashboardCardId = | "stats_bar" | "counters_bar" @@ -324,30 +332,6 @@ export type DashboardCardConfig = { defaultEnabled: boolean; }; -export type CardColSpan = "full" | "wide" | "half" | "narrow"; -export type CardRowSize = "short" | "medium" | "tall" | "flex"; - -export type CardLayoutConfig = { - id: DashboardCardId; - colSpan: CardColSpan; - rowSize: CardRowSize; - order: number; -}; - -export type LayoutPresetId = "default" | "compact" | "focus" | "wide"; - -export type LayoutPreset = { - id: LayoutPresetId; - label: string; - description: string; - cards: CardLayoutConfig[]; -}; - -export type UserProfileSection = - | "account" - | "appearance" - | "security" - | "api-keys"; export type AdminSection = | "general" | "sso" @@ -355,11 +339,12 @@ export type AdminSection = | "sessions" | "roles" | "host-defaults" + | "image-storage" | "database" | "api-keys" | "audit-log" - | "ssl"; -export type AccentColorId = string; + | "ssl" + | "touch-input"; export type ThemeId = | "dark" | "light" @@ -373,16 +358,83 @@ export type ThemeId = | "gruvbox"; export type FontSizeId = "xs" | "sm" | "md" | "lg" | "xl"; -export type ToolsTab = "ssh-tools" | "snippets" | "history" | "split-screen"; +export type ToolsTab = + "ssh-tools" | "snippets" | "macros" | "history" | "split-screen"; export type SplitMode = | "none" | "2-way" + | "2-way-horizontal" | "3-way" | "3-way-horizontal" | "4-way" | "5-way" | "6-way"; +export type SplitTabConfig = { + mode: Exclude; + paneTabIds: (string | null)[]; + rowSizes: number[]; + rowColSizes: number[][]; +}; + +export type WorkspaceTabSnapshot = { + /** Stable key within the saved tab list, not the live Tab.id (which is regenerated on every open). */ + slotId: string; + type: TabType; + /** Set for host-bound tab types, resolved by Host.syncId on apply. Never set for "serial". */ + hostSyncId?: string | null; + /** Denormalized snapshot for display and graceful-skip messaging if the host is later deleted. */ + hostNameSnapshot?: string | null; + label: string; + customLabel?: string; + initialFilePath?: string; + initialPath?: string; + fleetId?: number; + /** Only present when type === "serial". Fully self-contained, no host resolution needed. */ + serialConfig?: SerialConfig; +}; + +/** One dock's arrangement. `view` is a RailView, or null when the dock is closed. */ +export type WorkspaceDockState = { + view: string | null; + open: boolean; + width: number; +}; + +export type WorkspacePayload = { + version: 1; + tabs: WorkspaceTabSnapshot[]; + activeSlotId: string | null; + splitMode: SplitMode; + /** Indexed identically to AppShell's paneTabIds (length 6), holding slotId instead of a live Tab.id. */ + paneTabIds: (string | null)[]; + rowSizes: number[]; + rowColSizes: number[][]; + /** Sidebar arrangement, so a workspace restores the whole layout and not just tabs. */ + sidebar?: { + left: WorkspaceDockState; + right: WorkspaceDockState; + }; +}; + +export type WorkspaceKind = "manual" | "last_session"; + +export type Workspace = { + id: number; + userId: string; + name: string; + color: string | null; + icon: string | null; + kind: WorkspaceKind; + isDefault: boolean; + payload: WorkspacePayload; + syncId: string | null; + createdAt: string; + updatedAt: string; + lastUsedAt: string | null; + tabCount: number; +}; + export type Snippet = { id: number; name: string; @@ -391,6 +443,7 @@ export type Snippet = { folder: string | null; order: number; hostIds?: number[]; + isNote?: boolean; }; export const FOLDER_ICONS = [ @@ -414,10 +467,3 @@ export type SnippetFolder = { icon: FolderIconId; open: boolean; }; - -export type HistoryEntry = { - id: number; - command: string; - host: string; - time: string; -}; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index 5c905e5..ca74e3c 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -5,7 +5,15 @@ import { useTranslation } from "react-i18next"; import { Separator } from "@/components/separator"; import { Button } from "@/components/button"; import { Sheet, SheetContent } from "@/components/sheet"; -import { ChevronLeft, ChevronRight, Maximize2 } from "lucide-react"; +import { + ChevronLeft, + ChevronRight, + Maximize2, + Minimize2, + PanelRight, + RotateCcw, + SquareArrowOutUpRight, +} from "lucide-react"; import { useState, useRef, @@ -17,10 +25,19 @@ import { } from "react"; import { createPortal } from "react-dom"; import { useIsMobile } from "@/hooks/use-mobile"; +import { useAiAvailability } from "@/hooks/use-ai-availability"; import { MobileBottomBar } from "@/shell/MobileBottomBar"; -import { AppRail } from "@/sidebar/AppRail"; -import type { RailView } from "@/sidebar/AppRail"; -import { SplitView } from "@/shell/SplitView"; +import { AppRail, type RailView } from "@/sidebar/AppRail"; +import { + railItemLabel, + PROMOTABLE_IDS, + RIGHT_DOCKABLE_IDS, +} from "@/sidebar/rail-items"; +import { MultiPanelHint } from "@/sidebar/MultiPanelHint"; +import { OnboardingDialog } from "@/onboarding/OnboardingDialog"; +import { UI_ONBOARDING_VERSION } from "@/types/ui-preferences"; +import { useUiPreferencesContext } from "@/contexts/UiPreferencesContext"; +import { defaultSizes, SplitView, type RowColSizes } from "@/shell/SplitView"; import { renderTabContent } from "@/shell/tabUtils"; import { TabBar } from "@/shell/TabBar"; @@ -59,6 +76,27 @@ const SshToolsPanel = lazy(() => const SnippetsPanel = lazy(() => import("@/sidebar/SnippetsPanel").then((m) => ({ default: m.SnippetsPanel })), ); +const MacrosPanel = lazy(() => + import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })), +); +const FleetsPanel = lazy(() => + import("@/sidebar/FleetsPanel").then((m) => ({ default: m.FleetsPanel })), +); +const WorkspacesPanel = lazy(() => + import("@/sidebar/WorkspacesPanel").then((m) => ({ + default: m.WorkspacesPanel, + })), +); +const AutomationsPanel = lazy(() => + import("@/sidebar/AutomationsPanel").then((m) => ({ + default: m.AutomationsPanel, + })), +); +const AiPanel = lazy(() => + import("@/features/ai/AiPanel").then((m) => ({ + default: m.AiPanel, + })), +); const HistoryPanel = lazy(() => import("@/sidebar/HistoryPanel").then((m) => ({ default: m.HistoryPanel })), ); @@ -110,6 +148,8 @@ import type { ThemeId, FontSizeId, SerialConfig, + Workspace, + WorkspacePayload, } from "@/types/ui-types"; import { applyAccentColor, applyFontSize, PANE_COUNTS } from "@/lib/theme"; import { globalShortcutHandler } from "@/lib/global-shortcut-handler"; @@ -125,63 +165,43 @@ import { createSSHHost, getActiveSessions, getUserPreferences, + saveUserPreferences, dismissDonationModal, + isElectron, type UserPreferences, type OpenTabRecord, } from "@/main-axios"; +import { + listWorkspaces, + applyWorkspaceServer, + saveLastSessionWorkspace, +} from "@/api/workspaces-api"; +import { + buildWorkspacePayload as buildWorkspacePayloadUtil, + remapSlotIds, + resolveWorkspaceTabTarget, +} from "@/shell/workspaceUtils"; import { DonationReminderModal } from "@/user/DonationReminderModal.tsx"; +import { RemoteSyncBanner } from "@/components/RemoteSyncBanner.tsx"; +import { MigrationNoticeDialog } from "@/components/MigrationNoticeDialog.tsx"; import { dbHealthMonitor } from "@/lib/db-health-monitor"; -import type { SSHHostWithStatus } from "@/main-axios"; import { ServerStatusProvider } from "@/lib/ServerStatusContext"; import { TransferMonitor } from "@/features/file-manager/TransferMonitor.tsx"; import { sshHostToHost } from "@/sidebar/HostManagerData"; import { resolveHostTabType } from "@/lib/host-connection-tabs"; -import { changeAppLanguage } from "@/i18n/i18n"; +import { changeAppLanguage, consumeLoginLanguage } from "@/i18n/i18n"; import { quickConnectHostToPayload } from "@/sidebar/quick-connect-host"; +import { buildHostTree } from "@/sidebar/build-host-tree"; +import { + assignTabsToSplit, + createSplitConfig, + releaseSplitTabs, + restoreSplitTabs, + serializeSplitTabs, + type PersistedSplitTab, +} from "@/shell/splitTabUtils"; -function buildHostTree( - hosts: SSHHostWithStatus[], - folderMeta?: Map, -): HostFolder { - const root: HostFolder = { name: "root", children: [] }; - const folderMap = new Map(); - const getOrCreateFolder = (path: string): HostFolder => { - if (folderMap.has(path)) return folderMap.get(path)!; - const parts = path.split(" / "); - let current = root; - let accumulated = ""; - for (const part of parts) { - accumulated = accumulated ? `${accumulated} / ${part}` : part; - if (!folderMap.has(accumulated)) { - const meta = folderMeta?.get(accumulated); - const folder: HostFolder = { - name: part, - path: accumulated, - color: meta?.color, - icon: meta?.icon, - children: [], - }; - folderMap.set(accumulated, folder); - current.children.push(folder); - } - current = folderMap.get(accumulated)!; - } - return current; - }; - // Surface empty folders (created but with no hosts yet) so they stay visible. - if (folderMeta) { - for (const path of folderMeta.keys()) getOrCreateFolder(path); - } - for (const h of hosts) { - const host = sshHostToHost(h); - if (h.folder) { - getOrCreateFolder(h.folder).children.push(host); - } else { - root.children.push(host); - } - } - return root; -} +export { buildHostTree } from "@/sidebar/build-host-tree"; export { tabIcon, renderTabContent } from "@/shell/tabUtils"; // โ”€โ”€โ”€ AppShell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -189,14 +209,14 @@ export { tabIcon, renderTabContent } from "@/shell/tabUtils"; export function AppShell({ username, onLogout, - onChangeServer, }: { username: string; onLogout: () => void; - onChangeServer?: () => void; }) { const { t, i18n } = useTranslation(); const { setTheme } = useTheme(); + const { globallyEnabled: aiGloballyEnabled, loaded: aiStatusLoaded } = + useAiAvailability(); const [tabs, setTabs] = useState([ { id: "dashboard", @@ -215,36 +235,120 @@ export function AppShell({ // Flips to true once the initial DB read (restore or skip) is done โ€” sync must not fire before this const [tabsReady, setTabsReady] = useState(false); const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); - const [splitMode, setSplitMode] = useState( - () => (localStorage.getItem("termix_splitMode") as SplitMode) ?? "none", - ); - const [paneTabIds, setPaneTabIds] = useState<(string | null)[]>( - () => - JSON.parse(localStorage.getItem("termix_paneTabIds") ?? "null") ?? - Array(6).fill(null), + const [splitMode, setSplitMode] = useState("none"); + // paneTabIds holds live tab.id values, which change on every restore, so we + // can't restore it from storage directly. It starts empty and gets filled in + // once by the reconciliation effect below, keyed off the stable instanceId + // values saved in termix_paneInstanceIds. + const [paneTabIds, setPaneTabIds] = useState<(string | null)[]>(() => + Array(6).fill(null), ); + const paneLayoutRestoredRef = useRef(false); + const splitTabsRestoredRef = useRef(false); useEffect(() => { paneTabIdsRef.current = paneTabIds; }, [paneTabIds]); + const [rowSizes, setRowSizes] = useState( + () => defaultSizes("none").rowSizes, + ); + const [rowColSizes, setRowColSizes] = useState( + () => defaultSizes("none").rowColSizes, + ); + const changeSplitMode = useCallback((mode: SplitMode) => { + setSplitMode(mode); + const d = defaultSizes(mode); + setRowSizes(d.rowSizes); + setRowColSizes(d.rowColSizes); + }, []); const [focusedPaneIndex, setFocusedPaneIndex] = useState(null); const [realHostTree, setRealHostTree] = useState(null); const [hostsLoading, setHostsLoading] = useState(true); const [allHosts, setAllHosts] = useState([]); const [isAdmin, setIsAdmin] = useState(false); + // The standalone desktop backend still owns system settings such as the + // Tailscale API key, even though it has only one implicit user. + const showAdminUI = isAdmin; const [userId, setUserId] = useState(null); const [showDonationModal, setShowDonationModal] = useState(false); + const [showOnboarding, setShowOnboarding] = useState(false); + const [onboardingAiEnabled, setOnboardingAiEnabled] = useState(false); const [backgroundTabRecords, setBackgroundTabRecords] = useState< OpenTabRecord[] >([]); + // First-run onboarding. The backend hands accounts that predate this feature + // an already-completed state, so only genuinely new users are interrupted. + const uiPrefs = useUiPreferencesContext(); + const onboardingPending = + !!uiPrefs?.loaded && + uiPrefs.preferences.onboarding.completedVersion < UI_ONBOARDING_VERSION; + + /** + * Both onboarding entry points resolve the same context first: whether an + * admin has enabled the AI assistant. The AI step is skipped entirely when + * it is off, so the answer has to be in before the dialog opens. + */ + const loadOnboardingContext = useCallback(async () => { + try { + const { getAiStatus } = await import("@/api/ai-api"); + const aiStatus = await getAiStatus(); + setOnboardingAiEnabled(aiStatus.globallyEnabled); + } catch { + setOnboardingAiEnabled(false); + } + }, []); + + useEffect(() => { + if (!username || !onboardingPending) return; + let cancelled = false; + loadOnboardingContext().finally(() => { + if (!cancelled) setShowOnboarding(true); + }); + return () => { + cancelled = true; + }; + }, [username, onboardingPending, loadOnboardingContext]); + + // "Run setup again" from settings. + useEffect(() => { + const handler = () => { + loadOnboardingContext().finally(() => setShowOnboarding(true)); + }; + window.addEventListener("termix:open-onboarding", handler); + return () => window.removeEventListener("termix:open-onboarding", handler); + }, [loadOnboardingContext]); + const [sidebarOpen, setSidebarOpen] = useState(true); const [railView, setRailView] = useState("hosts"); + const [remoteSyncInitialServerUrl, setRemoteSyncInitialServerUrl] = useState< + string | undefined + >(undefined); const [sidebarWidth, setSidebarWidth] = useState(() => { const saved = localStorage.getItem("termix_sidebarWidth"); return saved ? parseInt(saved, 10) : 291; }); const [sidebarDragging, setSidebarDragging] = useState(false); const [sidebarEditing, setSidebarEditing] = useState(false); + const [settingsFullscreen, setSettingsFullscreen] = useState(false); + + // Right dock โ€” a second panel column so reference panels like history can + // stay visible while the left sidebar is used for something else. + const [rightRailView, setRightRailView] = useState(() => { + const saved = localStorage.getItem("termix_rightRailView"); + return saved && RIGHT_DOCKABLE_IDS.includes(saved) + ? (saved as RailView) + : null; + }); + const [rightSidebarWidth, setRightSidebarWidth] = useState(() => { + const saved = localStorage.getItem("termix_rightSidebarWidth"); + return saved ? parseInt(saved, 10) : 291; + }); + const [rightSidebarDragging, setRightSidebarDragging] = useState(false); + // Remembers the last panel shown in the dock so the tab bar toggle can bring + // it back instead of always falling back to the same default. + const lastRightRailViewRef = useRef( + localStorage.getItem("termix_lastRightRailView"), + ); const [isAppFullscreen, setIsAppFullscreen] = useState( () => !!document.fullscreenElement, ); @@ -254,14 +358,43 @@ export function AppShell({ }, [sidebarWidth]); useEffect(() => { - localStorage.setItem("termix_splitMode", splitMode); - }, [splitMode]); + localStorage.setItem("termix_rightSidebarWidth", String(rightSidebarWidth)); + }, [rightSidebarWidth]); useEffect(() => { - localStorage.setItem("termix_paneTabIds", JSON.stringify(paneTabIds)); - }, [paneTabIds]); + if (rightRailView) { + localStorage.setItem("termix_rightRailView", rightRailView); + lastRightRailViewRef.current = rightRailView; + localStorage.setItem("termix_lastRightRailView", rightRailView); + } else { + localStorage.removeItem("termix_rightRailView"); + } + }, [rightRailView]); + + useEffect(() => { + if (!splitTabsRestoredRef.current) return; + localStorage.setItem( + "termix_splitTabs", + JSON.stringify(serializeSplitTabs(tabs)), + ); + }, [tabs]); const isMobile = useIsMobile(); + const isSettingsView = + railView === "user-profile" || railView === "admin-settings"; + + useEffect(() => { + if (!settingsFullscreen) return; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") setSettingsFullscreen(false); + }; + window.addEventListener("keydown", closeOnEscape); + return () => window.removeEventListener("keydown", closeOnEscape); + }, [settingsFullscreen]); + + useEffect(() => { + if (!isSettingsView) setSettingsFullscreen(false); + }, [isSettingsView]); const sidebarOpenBeforeMobile = useRef(sidebarOpen); useEffect(() => { @@ -319,6 +452,7 @@ export function AppShell({ const lastShiftTime = useRef(0); const tabsRef = useRef(tabs); const activeTabIdRef = useRef(activeTabId); + const closeActiveTabRef = useRef<() => void>(() => {}); const splitModeRef = useRef(splitMode); const focusedPaneIndexRef = useRef(null); const paneContentElsRef = useRef<(HTMLDivElement | null)[]>( @@ -331,6 +465,56 @@ export function AppShell({ useEffect(() => { activeTabIdRef.current = activeTabId; }, [activeTabId]); + useEffect(() => { + return window.electronAPI?.onCloseActiveTab?.(() => + closeActiveTabRef.current(), + ); + }, []); + const skipSplitSyncRef = useRef(false); + useEffect(() => { + const active = tabsRef.current.find((tab) => tab.id === activeTabId); + const config = active?.type === "split-screen" ? active.splitConfig : null; + skipSplitSyncRef.current = true; + if (!config) { + setSplitMode("none"); + setPaneTabIds(Array(6).fill(null)); + setFocusedPaneIndex(null); + return; + } + setSplitMode(config.mode); + setPaneTabIds(config.paneTabIds); + setRowSizes(config.rowSizes); + setRowColSizes(config.rowColSizes); + setFocusedPaneIndex(0); + }, [activeTabId]); + + useEffect(() => { + if (skipSplitSyncRef.current) { + skipSplitSyncRef.current = false; + return; + } + if (splitMode === "none") return; + setTabs((prev) => { + const active = prev.find((tab) => tab.id === activeTabId); + if (active?.type !== "split-screen") return prev; + const config = createSplitConfig(splitMode, paneTabIds, { + rowSizes, + rowColSizes, + }); + const updated = prev.map((tab) => + tab.id === activeTabId ? { ...tab, splitConfig: config } : tab, + ); + return assignTabsToSplit(updated, activeTabId, paneTabIds); + }); + }, [activeTabId, paneTabIds, rowColSizes, rowSizes, splitMode]); + // Panels like history and snippets act on "the terminal you're working in". + // Once those panels can themselves be the active tab, activeTabId points at + // the panel and the lookup misses, so remember the last terminal instead. + const [lastTerminalTabId, setLastTerminalTabId] = useState(activeTabId); + useEffect(() => { + const active = tabs.find((t) => t.id === activeTabId); + if (active?.type === "terminal") setLastTerminalTabId(active.id); + }, [activeTabId, tabs]); useEffect(() => { splitModeRef.current = splitMode; }, [splitMode]); @@ -383,24 +567,12 @@ export function AppShell({ [], ); - const sidebarTitle: Record = { - hosts: "Hosts", - credentials: "Credentials", - "termix-id": t("nav.termixId"), - "quick-connect": "Quick Connect", - serial: t("nav.serial"), - "ssh-tools": "SSH Tools", - snippets: "Snippets", - history: "History", - "session-logs": t("nav.sessionLogs"), - "split-screen": "Split Screen", - connections: t("nav.connections"), - "user-profile": "User Profile", - "admin-settings": "Admin Settings", - alerts: t("nav.alerts"), - }; + // Titles come from the shared rail definitions so they stay translated and + // in step with the rail itself. + const sidebarTitle = (view: RailView): string => railItemLabel(view, t); - // Double-shift opens command palette + // Double-shift or Ctrl+K opens the command palette. Double-shift alone was + // hard to discover. useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.code === "ShiftLeft" && !e.repeat) { @@ -409,11 +581,45 @@ export function AppShell({ setCommandPaletteOpen((prev) => !prev); lastShiftTime.current = now; } + if ( + (e.ctrlKey || e.metaKey) && + !e.shiftKey && + !e.altKey && + e.code === "KeyK" && + commandPaletteShortcutEnabled + ) { + e.preventDefault(); + setCommandPaletteOpen((prev) => !prev); + } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [commandPaletteShortcutEnabled]); + // Ctrl+Shift+E toggles between the two most recent sidebar panels. + const previousRailViewRef = useRef(null); + const currentRailViewRef = useRef(railView); + useEffect(() => { + if (currentRailViewRef.current !== railView) { + previousRailViewRef.current = currentRailViewRef.current; + currentRailViewRef.current = railView; + } + }, [railView]); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!e.ctrlKey || !e.shiftKey || e.altKey || e.code !== "KeyE") return; + e.preventDefault(); + const previous = previousRailViewRef.current; + if (!sidebarOpen) { + setSidebarOpen(true); + return; + } + if (previous && previous !== railView) handleRailClick(previous); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [railView, sidebarOpen]); + // Split-screen and tab navigation hotkeys // Also registered in globalShortcutHandler so xterm can invoke directly // without going through synthetic DOM events (which are unreliable). @@ -431,27 +637,9 @@ export function AppShell({ if (e.ctrlKey && e.shiftKey && !e.altKey && e.code === "Backslash") { e.preventDefault(); if (splitModeRef.current !== "none") { - splitModeRef.current = "none"; - setSplitMode("none"); - setPaneTabIds(Array(6).fill(null)); + selectSplitMode("none"); } else { - const mode = "2-way"; - splitModeRef.current = mode; - const currentTabs = tabsRef.current; - const currentActiveId = activeTabIdRef.current; - const count = PANE_COUNTS[mode]; - const next: (string | null)[] = Array(6).fill(null); - next[0] = currentActiveId; - let slot = 1; - for (const tab of currentTabs) { - if (slot >= count) break; - if (tab.id !== currentActiveId && tab.type !== "dashboard") { - next[slot] = tab.id; - slot++; - } - } - setSplitMode(mode); - setPaneTabIds(next); + selectSplitMode("2-way"); } return; } @@ -460,27 +648,9 @@ export function AppShell({ if (e.ctrlKey && e.shiftKey && !e.altKey && e.code === "Minus") { e.preventDefault(); if (splitModeRef.current !== "none") { - splitModeRef.current = "none"; - setSplitMode("none"); - setPaneTabIds(Array(6).fill(null)); + selectSplitMode("none"); } else { - const mode = "3-way-horizontal"; - splitModeRef.current = mode; - const currentTabs = tabsRef.current; - const currentActiveId = activeTabIdRef.current; - const count = PANE_COUNTS[mode]; - const next: (string | null)[] = Array(6).fill(null); - next[0] = currentActiveId; - let slot = 1; - for (const tab of currentTabs) { - if (slot >= count) break; - if (tab.id !== currentActiveId && tab.type !== "dashboard") { - next[slot] = tab.id; - slot++; - } - } - setSplitMode(mode); - setPaneTabIds(next); + selectSplitMode("3-way-horizontal"); } return; } @@ -508,6 +678,10 @@ export function AppShell({ [null, 1, null, null], [0, null, null, null], ], + "2-way-horizontal": [ + [null, null, null, 1], + [null, null, 0, null], + ], "3-way": [ [null, 1, null, null], [0, null, null, 2], @@ -556,12 +730,23 @@ export function AppShell({ const termRef = terminalRefs.current.get(tabId); ( termRef?.current as - | import("@/features/terminal/Terminal").TerminalHandle - | null + import("@/features/terminal/Terminal").TerminalHandle | null )?.focus(); } return; } + + // Alt+1..9 โ€” jump directly to the tab at that position + const digitMatch = /^Digit([1-9])$/.exec(e.code); + if (digitMatch) { + const currentTabs = tabsRef.current; + const index = Number(digitMatch[1]) - 1; + if (index < currentTabs.length) { + e.preventDefault(); + setActiveTabId(currentTabs[index].id); + } + return; + } } // Ctrl+Shift+] / Ctrl+Shift+[ โ€” cycle through open tabs (] = next, [ = previous) @@ -666,6 +851,7 @@ export function AppShell({ useEffect(() => { getUserPreferences() .then((prefs) => { + const loginLanguage = consumeLoginLanguage(); setUserPrefs(prefs); if (prefs.storageMode === "cloud") { // Persist the current browser values before overwriting, so any tab can restore them @@ -699,8 +885,12 @@ export function AppShell({ localStorage.setItem("termix-accent", prefs.accentColor); applyAccentColor(prefs.accentColor); } - if (prefs.language && prefs.language !== i18n.language) { - void changeAppLanguage(prefs.language); + const preferredLanguage = loginLanguage ?? prefs.language; + if (preferredLanguage && preferredLanguage !== i18n.language) { + void changeAppLanguage(preferredLanguage); + } + if (loginLanguage && loginLanguage !== prefs.language) { + void saveUserPreferences({ language: loginLanguage }); } if ( prefs.commandAutocomplete !== null && @@ -798,11 +988,21 @@ export function AppShell({ ]); const converted = raw.map(sshHostToHost); setAllHosts(converted); - const folderMeta = new Map(); + const folderMeta = new Map< + string, + { + color?: string; + icon?: string; + credentialId?: number | null; + sortOrder?: number | null; + } + >(); for (const f of folders) { folderMeta.set(f.name, { color: f.color ?? undefined, icon: f.icon ?? undefined, + credentialId: f.credentialId ?? null, + sortOrder: f.sortOrder ?? null, }); } setRealHostTree(buildHostTree(raw, folderMeta)); @@ -832,6 +1032,28 @@ export function AppShell({ }; }, [loadHosts]); + // The Electron main process runs remote sync (pull/push hosts and + // credentials with a connected Termix server) on its own timer, entirely + // outside any renderer-initiated action, so nothing normally dispatches + // the termix:hosts-changed / termix:credentials-changed events that + // panels rely on to refetch. Without this, newly-synced hosts/credentials + // only show up after a manual refresh or app restart. + useEffect(() => { + if (!isElectron()) return; + let wasSyncing = false; + const unsubscribe = window.electronAPI?.onRemoteSyncStatusChanged?.( + (status: { syncing: boolean; lastError: string | null }) => { + const justFinished = wasSyncing && !status.syncing && !status.lastError; + wasSyncing = status.syncing; + if (justFinished) { + window.dispatchEvent(new CustomEvent("termix:hosts-changed")); + window.dispatchEvent(new CustomEvent("termix:credentials-changed")); + } + }, + ); + return () => unsubscribe?.(); + }, []); + // Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings) useEffect(() => { if (allHosts.length === 0) return; @@ -868,6 +1090,132 @@ export function AppShell({ "tunnel", ]; + function buildWorkspacePayload(): WorkspacePayload { + return buildWorkspacePayloadUtil({ + tabs, + activeTabId, + splitMode, + paneTabIds, + rowSizes, + rowColSizes, + sidebar: { + left: { view: railView, open: sidebarOpen, width: sidebarWidth }, + right: { + view: rightRailView, + open: rightRailView !== null, + width: rightSidebarWidth, + }, + }, + }); + } + + async function applyWorkspace(workspace: Workspace) { + // Tear down the current arrangement the same way an individual tab close does. + for (const tab of [...tabsRef.current]) { + doCloseTab(tab.id); + } + + const slotIdToNewTabId = new Map(); + const skippedTabs: string[] = []; + + for (const snapshot of workspace.payload.tabs) { + const target = resolveWorkspaceTabTarget(snapshot, allHosts); + + if (target.kind === "skip") { + skippedTabs.push(snapshot.hostNameSnapshot || snapshot.label); + continue; + } + + if (target.kind === "serial" && snapshot.serialConfig) { + const newTabId = openSerialTab(snapshot.serialConfig); + slotIdToNewTabId.set(snapshot.slotId, newTabId); + continue; + } + + if (target.kind === "singleton") { + openSingletonTab( + snapshot.type, + undefined, + target.host, + snapshot.fleetId, + ); + slotIdToNewTabId.set(snapshot.slotId, snapshot.type); + continue; + } + + if (target.kind === "host") { + const newTabId = openTab(target.host, snapshot.type, { + instanceId: crypto.randomUUID(), + restoredSessionId: null, + savedLabel: snapshot.customLabel ?? snapshot.label, + initialFilePath: snapshot.initialFilePath, + initialPath: snapshot.initialPath, + }); + slotIdToNewTabId.set(snapshot.slotId, newTabId); + } + } + + const restoredPaneIds = remapSlotIds( + workspace.payload.paneTabIds, + slotIdToNewTabId, + ); + let restoredSplitTabId: string | null = null; + if ( + workspace.payload.splitMode !== "none" && + restoredPaneIds.some(Boolean) + ) { + const instanceId = crypto.randomUUID(); + restoredSplitTabId = `split-${instanceId}`; + const splitTab: Tab = { + id: restoredSplitTabId, + instanceId, + type: "split-screen", + label: workspace.name, + openedAt: Date.now(), + splitConfig: createSplitConfig( + workspace.payload.splitMode, + restoredPaneIds, + workspace.payload, + ), + }; + setTabs((prev) => + assignTabsToSplit([...prev, splitTab], splitTab.id, restoredPaneIds), + ); + } + + const activeId = workspace.payload.activeSlotId + ? (slotIdToNewTabId.get(workspace.payload.activeSlotId) ?? "dashboard") + : "dashboard"; + setActiveTabId(restoredSplitTabId ?? activeId); + + // Older payloads predate the sidebar field, so leave the docks alone then. + const sidebar = workspace.payload.sidebar; + if (sidebar) { + if (sidebar.left.view) setRailView(sidebar.left.view as RailView); + setSidebarOpen(sidebar.left.open); + if (sidebar.left.width) setSidebarWidth(sidebar.left.width); + + const right = sidebar.right.open ? sidebar.right.view : null; + setRightRailView( + right && RIGHT_DOCKABLE_IDS.includes(right) + ? (right as RailView) + : null, + ); + if (sidebar.right.width) setRightSidebarWidth(sidebar.right.width); + } + + if (skippedTabs.length > 0) { + toast.warning( + t("newUi.sidebar.workspaces.tabsSkipped", { + count: skippedTabs.length, + names: skippedTabs.join(", "), + }), + ); + } + + applyWorkspaceServer(workspace.id).catch(() => {}); + } + // On load: always read saved tabs from DB so background sessions are preserved across refreshes. // If reopenTabsOnLogin is on, also restore them as open tabs in the tab bar. const tabRestoreAttemptedRef = useRef(false); @@ -968,6 +1316,107 @@ export function AppShell({ loadSavedTabs(); }, [hostsLoaded, userPrefsLoaded]); + // If reopenTabsOnLogin didn't already restore anything (off, or on but + // nothing to restore), auto-apply the user's default workspace if they set + // one. Runs once, after the open-tabs restore above has had its chance โ€” + // that path wins when both would otherwise fire, since it's more granular + // and live-session-aware than a workspace snapshot. + const defaultWorkspaceAttemptedRef = useRef(false); + useEffect(() => { + if (!tabsReady || defaultWorkspaceAttemptedRef.current) return; + defaultWorkspaceAttemptedRef.current = true; + + const hasPersistentTabs = tabs.some((t) => + PERSISTENT_TAB_TYPES.includes(t.type), + ); + if (userPrefs.reopenTabsOnLogin && hasPersistentTabs) return; + + listWorkspaces() + .then((workspaces) => { + const defaultWorkspace = workspaces.find( + (w) => w.kind === "manual" && w.isDefault, + ); + if (defaultWorkspace) { + applyWorkspace(defaultWorkspace); + } + }) + .catch(() => {}); + }, [tabsReady]); + + // Restore named split tabs once their child sessions have stable live ids. The old + // singleton keys are migrated once into Split #1 so existing layouts are preserved. + useEffect(() => { + if (!tabsReady || paneLayoutRestoredRef.current) return; + paneLayoutRestoredRef.current = true; + + try { + const savedSplitTabs = JSON.parse( + localStorage.getItem("termix_splitTabs") ?? "[]", + ) as PersistedSplitTab[]; + if (Array.isArray(savedSplitTabs) && savedSplitTabs.length > 0) { + setTabs((prev) => restoreSplitTabs(savedSplitTabs, prev)); + splitTabsRestoredRef.current = true; + return; + } + + const savedInstanceIds: (string | null)[] = JSON.parse( + localStorage.getItem("termix_paneInstanceIds") ?? "null", + ); + const savedMode = localStorage.getItem("termix_splitMode") as SplitMode; + if ( + !Array.isArray(savedInstanceIds) || + !savedMode || + savedMode === "none" + ) { + splitTabsRestoredRef.current = true; + return; + } + + const restored = savedInstanceIds.map((instanceId) => { + if (instanceId == null) return null; + return tabs.find((t) => t.instanceId === instanceId)?.id ?? null; + }); + if (restored.some((id) => id != null)) { + let sizes = defaultSizes(savedMode); + try { + const savedSizes = JSON.parse( + localStorage.getItem("termix_paneSizes") ?? "null", + ) as { rowSizes?: number[]; rowColSizes?: RowColSizes } | null; + if ( + Array.isArray(savedSizes?.rowSizes) && + Array.isArray(savedSizes?.rowColSizes) + ) { + sizes = { + rowSizes: savedSizes.rowSizes, + rowColSizes: savedSizes.rowColSizes, + }; + } + } catch { + // silently fail + } + const instanceId = crypto.randomUUID(); + const id = `split-${instanceId}`; + const splitTab: Tab = { + id, + instanceId, + type: "split-screen", + label: "Split #1", + openedAt: Date.now(), + splitConfig: createSplitConfig(savedMode, restored, sizes), + }; + setTabs((prev) => assignTabsToSplit([...prev, splitTab], id, restored)); + setActiveTabId(id); + } + } catch { + // silently fail + } finally { + splitTabsRestoredRef.current = true; + localStorage.removeItem("termix_splitMode"); + localStorage.removeItem("termix_paneInstanceIds"); + localStorage.removeItem("termix_paneSizes"); + } + }, [tabsReady, tabs]); + // Debounced tab-order sync: when tab order changes, patch each persistent tab's tabOrder in DB. const orderSyncTimeoutRef = useRef | null>( null, @@ -995,6 +1444,39 @@ export function AppShell({ }; }, [tabs, tabsReady]); + // Debounced "Last Session" auto-save: keeps an implicit workspace snapshot + // current so the arrangement can always be recovered, even if the user never + // manually saves one. Never auto-applied on login โ€” see the default-workspace + // effect above, which only considers kind === "manual" rows. + const lastSessionSaveTimeoutRef = useRef | null>(null); + useEffect(() => { + if (!tabsReady) return; + if (lastSessionSaveTimeoutRef.current) + clearTimeout(lastSessionSaveTimeoutRef.current); + lastSessionSaveTimeoutRef.current = setTimeout(() => { + saveLastSessionWorkspace(buildWorkspacePayload()).catch(() => {}); + }, 2000); + + return () => { + if (lastSessionSaveTimeoutRef.current) + clearTimeout(lastSessionSaveTimeoutRef.current); + }; + }, [ + tabs, + paneTabIds, + splitMode, + rowSizes, + rowColSizes, + tabsReady, + railView, + sidebarOpen, + sidebarWidth, + rightRailView, + rightSidebarWidth, + ]); + // โ”€โ”€โ”€ Tab management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const openTab = useCallback(function openTab( @@ -1005,7 +1487,10 @@ export function AppShell({ restoredSessionId: string | null; savedLabel?: string; initialFilePath?: string; + initialPath?: string; serialConfig?: SerialConfig; + joinSharedSessionId?: string | null; + joinShareId?: string | null; }, ) { const tabId = `${host.name}-${type}-${Date.now()}`; @@ -1021,7 +1506,10 @@ export function AppShell({ let finalLabel = host.name; const savedLabel = restore?.savedLabel; const initialFilePath = restore?.initialFilePath; + const initialPath = restore?.initialPath; const serialConfig = restore?.serialConfig; + const joinSharedSessionId = restore?.joinSharedSessionId ?? null; + const joinShareId = restore?.joinShareId ?? null; // A saved label that doesn't match the bare host name or the auto-numbered pattern is a custom label const isCustomLabel = savedLabel != null && @@ -1043,7 +1531,10 @@ export function AppShell({ openedAt, terminalRef: ref, restoredSessionId: restore?.restoredSessionId ?? null, + joinSharedSessionId, + joinShareId, initialFilePath, + initialPath, serialConfig, }, ]; @@ -1075,7 +1566,10 @@ export function AppShell({ openedAt, terminalRef: ref, restoredSessionId: restore?.restoredSessionId ?? null, + joinSharedSessionId, + joinShareId, initialFilePath, + initialPath, serialConfig, }, ]; @@ -1091,6 +1585,8 @@ export function AppShell({ tabOrder: 0, }).catch(() => {}); } + + return tabId; }, []); function connectHost(host: Host, preferredType?: TabType) { @@ -1118,7 +1614,7 @@ export function AppShell({ [loadHosts, t], ); - function openSerialTab(config: SerialConfig) { + function openSerialTab(config: SerialConfig): string { const pseudoHost: Host = { id: `serial-${Date.now()}`, name: config.path @@ -1139,7 +1635,9 @@ export function AppShell({ enableFileManager: false, enableDocker: false, enableProxmox: false, + enableProxmoxStats: false, enableTmuxMonitor: false, + enableTerminalToolbar: false, enableSsh: false, enableRdp: false, enableVnc: false, @@ -1155,13 +1653,41 @@ export function AppShell({ typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; - openTab(pseudoHost, "serial", { + return openTab(pseudoHost, "serial", { instanceId, restoredSessionId: null, serialConfig: config, }); } + function openLocalTerminalTab(): string { + const instanceId = + typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; + const id = `local-terminal-${instanceId}`; + setTabs((current) => { + const count = current.filter( + (tab) => tab.type === "local-terminal", + ).length; + return [ + ...current, + { + id, + instanceId, + type: "local-terminal", + label: + count === 0 + ? t("nav.localTerminal") + : `${t("nav.localTerminal")} (${count + 1})`, + openedAt: Date.now(), + }, + ]; + }); + setActiveTabId(id); + return id; + } + const openSingletonTab = useCallback( // --- tmux-monitor --- (added optional `host` so tmux_monitor can open // with a preselected host; existing callers are unaffected) @@ -1169,7 +1695,15 @@ export function AppShell({ type: TabType, pendingEvent?: string, host?: Host, + fleetId?: number, ) { + // Local terminals are never singletons, each one is its own shell. + if (type === "local-terminal") { + return openLocalTerminalTab(); + } + // The admin kill switch removes the assistant for everyone, so it can + // never be promoted into the tab bar while it is off. + if (type === "ai" && !aiGloballyEnabled) return; if (type === "host-manager") { if (pendingEvent === "host-manager:add-credential") { setSidebarOpen(true); @@ -1210,13 +1744,24 @@ export function AppShell({ network_graph: t("nav.networkGraph"), tmux_monitor: t("nav.tmuxMonitor"), // --- tmux-monitor --- homepage: t("nav.homepage"), + "fleet-inventory": t("nav.fleets"), }; + // Promoted rail panels reuse the rail's own label so the two stay in sync. + const label = singletonLabels[type] ?? railItemLabel(type, t); setTabs((prev) => { const existing = prev.find((t) => t.id === id); if (existing) { // --- tmux-monitor --- refocusing with a host preselects it - if (!host) return prev; - return prev.map((t) => (t.id === id ? { ...t, host } : t)); + if (!host && fleetId === undefined) return prev; + return prev.map((t) => + t.id === id + ? { + ...t, + ...(host ? { host } : {}), + ...(fleetId !== undefined ? { fleetId } : {}), + } + : t, + ); } return [ ...prev, @@ -1224,9 +1769,10 @@ export function AppShell({ id, instanceId: id, type, - label: singletonLabels[type] ?? type, + label, openedAt: Date.now(), ...(host ? { host } : {}), // --- tmux-monitor --- + ...(fleetId !== undefined ? { fleetId } : {}), }, ]; }); @@ -1236,12 +1782,12 @@ export function AppShell({ id, tabType: type, hostId: null, - label: singletonLabels[type] ?? type, + label, tabOrder: 0, }).catch(() => {}); } }, - [t], + [t, aiGloballyEnabled], ); const SESSION_TAB_TYPES: TabType[] = [ @@ -1295,14 +1841,33 @@ export function AppShell({ terminalRefs.current.delete(id); if (id === activeTabId) { - const remaining = tabs.filter((t) => t.id !== id); + const remaining = tabs.filter( + (tab) => tab.id !== id && !tab.parentSplitTabId, + ); setActiveTabId( remaining.length > 0 ? remaining[remaining.length - 1].id : "dashboard", ); } setPaneTabIds((prev) => prev.map((p) => (p === id ? null : p))); setTabs((prev) => { - const next = prev.filter((t) => t.id !== id); + const next = + tabToClose?.type === "split-screen" + ? releaseSplitTabs(prev, id) + : prev + .filter((tab) => tab.id !== id) + .map((tab) => + tab.type === "split-screen" && tab.splitConfig + ? { + ...tab, + splitConfig: { + ...tab.splitConfig, + paneTabIds: tab.splitConfig.paneTabIds.map((paneId) => + paneId === id ? null : paneId, + ), + }, + } + : tab, + ); if (next.length === 0) return [ { @@ -1330,6 +1895,17 @@ export function AppShell({ } } + function openShareForTab(id: string) { + const tab = tabs.find((t) => t.id === id); + if (!tab) return; + const ref = tab.terminalRef?.current; + if (ref?.canShare?.()) { + ref.openShareModal?.(); + } else { + toast.error(t("sessionSharing.notReadyToShare")); + } + } + function closeTab(id: string) { const tab = tabs.find((t) => t.id === id); const confirmEnabled = localStorage.getItem("confirmTabClose") === "true"; @@ -1367,6 +1943,21 @@ export function AppShell({ doCloseTab(id); } + // An admin can turn the assistant off while tabs are already open, and a + // saved workspace or restored session can bring one back. Either way the + // leftover tab and panel go away as soon as the status says it is off. + useEffect(() => { + if (!aiStatusLoaded || aiGloballyEnabled) return; + if (tabs.some((tab) => tab.type === "ai")) doCloseTab("ai"); + setRailView((prev) => (prev === "ai" ? "hosts" : prev)); + setRightRailView((prev) => (prev === "ai" ? null : prev)); + }, [aiStatusLoaded, aiGloballyEnabled, tabs]); + + closeActiveTabRef.current = () => { + const id = activeTabIdRef.current; + if (id !== "dashboard") closeTab(id); + }; + function renameTab(tabId: string, newLabel: string) { setTabs((prev) => prev.map((t) => @@ -1374,31 +1965,54 @@ export function AppShell({ ), ); const tab = tabs.find((t) => t.id === tabId); - if (tab?.instanceId) { + if (tab?.instanceId && tab.type !== "split-screen") { patchOpenTab(tab.instanceId, { label: newLabel }).catch(() => {}); } } function splitTabQuick(tabId: string, mode: SplitMode) { - setSplitMode(mode); - setPaneTabIds(() => { - const count = PANE_COUNTS[mode]; - const next: (string | null)[] = Array(6).fill(null); - next[0] = tabId; - // Fill remaining panes with other non-dashboard tabs in order - let slot = 1; - for (const tab of tabs) { - if (slot >= count) break; - if (tab.id !== tabId && tab.type !== "dashboard") { - next[slot] = tab.id; - slot++; - } + if (mode === "none") return; + const count = PANE_COUNTS[mode]; + const paneIds: (string | null)[] = Array(6).fill(null); + paneIds[0] = tabId; + let slot = 1; + for (const tab of tabs) { + if (slot >= count) break; + if ( + tab.id !== tabId && + tab.type !== "dashboard" && + tab.type !== "split-screen" && + !tab.parentSplitTabId + ) { + paneIds[slot++] = tab.id; } - return next; - }); + } + const splitNumber = + tabs.filter((tab) => tab.type === "split-screen").length + 1; + const instanceId = crypto.randomUUID(); + const id = `split-${instanceId}`; + const sizes = defaultSizes(mode); + const splitTab: Tab = { + id, + instanceId, + type: "split-screen", + label: `Split #${splitNumber}`, + openedAt: Date.now(), + splitConfig: createSplitConfig(mode, paneIds, sizes), + }; + setTabs((prev) => assignTabsToSplit([...prev, splitTab], id, paneIds)); + setActiveTabId(id); + setSplitMode(mode); + setPaneTabIds(paneIds); + setRowSizes(sizes.rowSizes); + setRowColSizes(sizes.rowColSizes); } function addTabToSplit(tabId: string) { + if (splitMode === "none") { + splitTabQuick(tabId, "2-way"); + return; + } setPaneTabIds((prev) => { // Remove from any current slot first const next = prev.map((p) => (p === tabId ? null : p)); @@ -1418,21 +2032,66 @@ export function AppShell({ setPaneTabIds((prev) => prev.map((p) => (p === tabId ? null : p))); } + function selectSplitMode(mode: SplitMode) { + const active = tabs.find((tab) => tab.id === activeTabId); + if (mode === "none") { + if (active?.type === "split-screen") doCloseTab(active.id); + return; + } + if (active?.type === "split-screen") { + changeSplitMode(mode); + return; + } + if (active && active.type !== "dashboard") { + splitTabQuick(active.id, mode); + return; + } + const firstSession = tabs.find( + (tab) => + tab.type !== "dashboard" && + tab.type !== "split-screen" && + !tab.parentSplitTabId, + ); + if (firstSession) splitTabQuick(firstSession.id, mode); + } + function assignPane(paneIndex: number, tabId: string) { setPaneTabIds((prev) => { const next = prev.map((p) => (p === tabId ? null : p)); - next[paneIndex] = tabId; + next[paneIndex] = tabId || null; return next; }); } // โ”€โ”€โ”€ Rail / sidebar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Moving a panel to the right dock rather than copying it: two live copies of + // the same panel would fight over the shared editing state. + function openInRightDock(view: RailView) { + setRightRailView(view); + if (railView === view) setSidebarOpen(false); + } + + // Tab bar toggle: reopens whatever was last in the dock, so it behaves like a + // show/hide rather than losing the user's choice each time. + function toggleRightDock() { + if (rightRailView) { + lastRightRailViewRef.current = rightRailView; + setRightRailView(null); + return; + } + const fallback = lastRightRailViewRef.current ?? RIGHT_DOCKABLE_IDS[0]; + if (fallback) setRightRailView(fallback as RailView); + } + function handleRailClick(view: RailView) { if (railView === view && sidebarOpen) { setSidebarOpen(false); } else { + // A panel lives in one dock at a time, so the left dock reclaims it. + if (rightRailView === view) setRightRailView(null); if (view !== railView) setSidebarEditing(false); + if (view !== railView) setSettingsFullscreen(false); setRailView(view); setSidebarOpen(true); } @@ -1470,6 +2129,29 @@ export function AppShell({ [sidebarWidth], ); + // Same drag, mirrored: the right dock grows as the pointer moves left. + const onRightSidebarMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setRightSidebarDragging(true); + const startX = e.clientX; + const startW = rightSidebarWidth; + function onMove(ev: MouseEvent) { + setRightSidebarWidth( + Math.max(160, Math.min(480, startW - (ev.clientX - startX))), + ); + } + function onUp() { + setRightSidebarDragging(false); + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + } + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }, + [rightSidebarWidth], + ); + // Resize all terminals in panes + active terminal when split mode or sidebar changes const resizeAllTerminals = useCallback(() => { const id = requestAnimationFrame(() => { @@ -1486,9 +2168,11 @@ export function AppShell({ useEffect(() => { const id = resizeAllTerminals(); return () => cancelAnimationFrame(id); - }, [splitMode, sidebarWidth, sidebarOpen]); + }, [splitMode, sidebarWidth, sidebarOpen, rightSidebarWidth, rightRailView]); - const isSplit = splitMode !== "none"; + const isSplit = + splitMode !== "none" && + tabs.some((tab) => tab.id === activeTabId && tab.type === "split-screen"); // Move each tab's stable DOM node to the right container (pane or normal-view). // This is vanilla DOM so React's portal target never changes โ€” changing the portal @@ -1508,7 +2192,8 @@ export function AppShell({ } for (const tab of tabs) { - const isTerminal = tab.type === "terminal"; + const isTerminal = + tab.type === "terminal" || tab.type === "local-terminal"; const node = getTabNode(tab.id, isTerminal); const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1; const inPane = paneIdx !== -1; @@ -1539,35 +2224,65 @@ export function AppShell({ }); const terminalTabs = tabs.filter((t) => t.type === "terminal"); + const topLevelTabs = tabs.filter((tab) => !tab.parentSplitTabId); - // Sidebar panel content โ€” shared between desktop inline sidebar and mobile sheet - const sidebarPanelContent = ( + function reorderTopLevelTabs(reordered: Tab[]) { + setTabs((prev) => [ + ...reordered, + ...prev.filter((tab) => tab.parentSplitTabId), + ]); + } + + // What history/snippets/ssh-tools should act on. Falls back to the remembered + // terminal when the active tab isn't one, and drops it once it's closed. + const targetTerminalTabId = terminalTabs.some((t) => t.id === activeTabId) + ? activeTabId + : terminalTabs.some((t) => t.id === lastTerminalTabId) + ? lastTerminalTabId + : ""; + + /** + * Sidebar panel content, shared between the desktop sidebar, the mobile + * sheet and the right dock. Takes the view rather than reading railView so + * both docks can render from the same code. + * + * `owned` marks the dock responsible for the panels that stay mounted while + * hidden (hosts, credentials, fleets). Only one dock may own them, otherwise + * two live instances fight over the shared editing state. + */ + // The param deliberately shadows the outer railView so the body reads the + // same whichever dock is rendering. + const renderSidebarPanels = (railView: RailView, owned = true) => ( }>
-
- { - connectHost(host, type); - if (isMobile) setSidebarOpen(false); - }} - onEditHost={editHostInManager} - hostTree={realHostTree ?? undefined} - loading={hostsLoading} - onEditingChange={setSidebarEditing} - active={railView === "hosts"} - /> -
+ {owned && ( + <> +
+ { + connectHost(host, type); + if (isMobile) setSidebarOpen(false); + }} + onEditHost={editHostInManager} + hostTree={realHostTree ?? undefined} + loading={hostsLoading} + onEditingChange={setSidebarEditing} + active={railView === "hosts"} + /> +
-
- -
+
+ +
+ + )} {railView === "termix-id" && (
@@ -1597,7 +2312,7 @@ export function AppShell({
)} @@ -1606,7 +2321,40 @@ export function AppShell({
+
+ )} + + {railView === "macros" && ( +
+ +
+ )} + + {owned && ( +
+ + openSingletonTab( + "fleet-inventory", + undefined, + undefined, + fleetId, + ) + } />
)} @@ -1615,7 +2363,7 @@ export function AppShell({
)} @@ -1623,9 +2371,14 @@ export function AppShell({ {railView === "split-screen" && (
+ tab.type !== "split-screen" && + (!tab.parentSplitTabId || + tab.parentSplitTabId === activeTabId), + )} splitMode={splitMode} - setSplitMode={setSplitMode} + setSplitMode={selectSplitMode} paneTabIds={paneTabIds} setPaneTabIds={setPaneTabIds} onAssignPane={assignPane} @@ -1633,6 +2386,35 @@ export function AppShell({
)} + {railView === "workspaces" && ( +
+ +
+ )} + + {railView === "automations" && ( +
+ +
+ )} + + {railView === "ai" && ( +
+ tab.id === activeTabId)?.type ?? null + } + /> +
+ )} + {railView === "connections" && (
{ + if (!session.shareId) return; + const existingHost = allHosts.find( + (h) => h.id === String(session.hostId), + ); + const host: Host = existingHost ?? { + id: String(session.hostId), + name: session.hostName, + username: "", + ip: "", + port: 0, + folder: "", + online: false, + cpu: null, + ram: null, + lastAccess: new Date().toISOString(), + authType: "none", + enableTerminal: false, + enableCommandHistory: false, + enableTunnel: false, + enableFileManager: false, + enableDocker: false, + enableProxmox: false, + enableProxmoxStats: false, + enableTmuxMonitor: false, + enableTerminalToolbar: false, + enableSsh: false, + enableRdp: false, + enableVnc: false, + enableTelnet: false, + sshPort: 22, + rdpPort: 3389, + vncPort: 5900, + telnetPort: 23, + serverTunnels: [], + quickActions: [], + }; + const instanceId = + typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; + openTab(host, "terminal", { + instanceId, + restoredSessionId: null, + joinSharedSessionId: session.sessionId, + joinShareId: session.shareId, + savedLabel: t("connections.sharedSessionLabel", { + hostName: session.hostName, + }), + }); + if (isMobile) setSidebarOpen(false); + }} />
)} @@ -1690,16 +2524,16 @@ export function AppShell({ setUserPrefs((current) => ({ ...current, ...updates })) } + remoteSyncInitialServerUrl={remoteSyncInitialServerUrl} />
)} - {railView === "admin-settings" && isAdmin && ( + {railView === "admin-settings" && showAdminUI && (
); + const sidebarPanelContent = renderSidebarPanels(railView); + // Sidebar header โ€” shared const sidebarHeader = (
- - {sidebarTitle[railView]} + + {sidebarTitle(railView)} + {!isMobile && PROMOTABLE_IDS.includes(railView) && ( + <> + + + + )} + {!isMobile && RIGHT_DOCKABLE_IDS.includes(railView) && ( + <> + + + + )} {!isMobile && ( <> @@ -1736,7 +2602,34 @@ export function AppShell({ title="Reset width" onClick={() => setSidebarWidth(291)} > - + + + + )} + {isSettingsView && ( + <> + + )} @@ -1745,189 +2638,294 @@ export function AppShell({ variant="ghost" size="icon" className="h-full w-12.5 rounded-none text-muted-foreground hover:text-foreground" - onClick={() => setSidebarOpen(false)} + onClick={() => { + setSettingsFullscreen(false); + setSidebarOpen(false); + }} >
); + const sidebarHint = !isMobile && ( + openSingletonTab(railView as TabType)} + onOpenInRightDock={() => openInRightDock(railView)} + /> + ); + return ( -
- {/* Skinny icon rail โ€” desktop only, hidden on mobile */} - - - {/* Desktop: inline resizable sidebar */} - {!isMobile && ( -
- {sidebarHeader} - {sidebarPanelContent} - - {sidebarOpen && !sidebarEditing && ( -
- )} -
+
+ {isElectron() && ( + <> + { + setRailView("user-profile"); + if (!sidebarOpen) setSidebarOpen(true); + }} + /> + { + setRemoteSyncInitialServerUrl(url); + setRailView("user-profile"); + if (!sidebarOpen) setSidebarOpen(true); + }} + /> + )} +
+ {/* Skinny icon rail โ€” desktop only, hidden on mobile */} + {!settingsFullscreen && ( + + )} - {/* Mobile: sidebar as overlay sheet */} - {isMobile && ( - - {sidebarHeader} + {sidebarHint} {sidebarPanelContent} - - - )} - {/* Main content area */} -
- {!isMobile && !sidebarOpen && ( - - )} -
- { - const targetTab = tabs.find((t) => t.id === tabId); - if (targetTab?.host) openTab(targetTab.host, "files"); - }} - isAppFullscreen={isAppFullscreen} - onToggleAppFullscreen={toggleAppFullscreen} - /> -
- {/* Split view โ€” always mounted when not mobile, hidden via CSS when inactive */} - {!isMobile && ( + {sidebarOpen && !sidebarEditing && !settingsFullscreen && (
- -
+ onMouseDown={onSidebarMouseDown} + className={`absolute right-0 top-0 bottom-0 w-1 cursor-col-resize z-30 transition-colors ${sidebarDragging ? "bg-accent-brand/60" : "hover:bg-accent-brand/40"}`} + /> )} +
+ )} - {/* Normal-view container. Tab nodes are appended here (or to pane elements) + {/* Mobile: sidebar as overlay sheet */} + {isMobile && ( + + + {sidebarHeader} + {sidebarPanelContent} + + + )} + + {/* Main content area */} +
+ {!isMobile && !sidebarOpen && ( + + )} +
+ { + const targetTab = tabs.find((t) => t.id === tabId); + if (targetTab?.host) openTab(targetTab.host, "files"); + }} + onOpenShare={openShareForTab} + isAppFullscreen={isAppFullscreen} + onToggleAppFullscreen={toggleAppFullscreen} + rightDockOpen={rightRailView !== null} + onToggleRightDock={isMobile ? undefined : toggleRightDock} + /> +
+ {/* Split view โ€” always mounted when not mobile, hidden via CSS when inactive */} + {!isMobile && ( +
+ changeSplitMode(splitMode)} + focusedPaneIndex={focusedPaneIndex} + onTerminalResize={resizeAllTerminals} + onPaneContentRef={onPaneContentRef} + onPaneClick={setFocusedPaneIndex} + onAssignPane={assignPane} + /> +
+ )} + + {/* Normal-view container. Tab nodes are appended here (or to pane elements) by the DOM-placement effect above. React portals each tab's content into its stable per-tab node so the component is never remounted. When split is active, shown on top only if the active tab is not in a pane. */} -
- {tabs.map((tab) => { - const tabNode = getTabNode(tab.id, tab.type === "terminal"); - const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1; - const inPane = paneIdx !== -1; - const activeInline = !inPane && tab.id === activeTabId; - return createPortal( - renderTabContent( - tab, - openSingletonTab, - openTab, - closeTab, - inPane || activeInline, - (host, filePath) => - openTab(host, "files", { - instanceId: - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, - restoredSessionId: null, - initialFilePath: filePath, - }), - (host, _path) => openTab(host, "files"), - (host, path) => - openTab(host, "terminal", { - instanceId: - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, - restoredSessionId: null, - initialFilePath: path, - }), - renameTab, - saveQuickConnectHost, - ), - tabNode, - tab.id, - ); - })} +
+ {tabs.map((tab) => { + const tabNode = getTabNode( + tab.id, + tab.type === "terminal" || tab.type === "local-terminal", + ); + const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1; + const inPane = paneIdx !== -1; + const activeInline = !inPane && tab.id === activeTabId; + const isFocusedPane = inPane + ? paneIdx === (focusedPaneIndex ?? 0) + : activeInline; + return createPortal( + renderTabContent( + tab, + openSingletonTab, + openTab, + closeTab, + inPane || activeInline, + (host, filePath) => + openTab(host, "files", { + instanceId: + typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, + restoredSessionId: null, + initialFilePath: filePath, + }), + (host, path) => + openTab(host, "files", { + instanceId: + typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, + restoredSessionId: null, + initialPath: path, + }), + (host, path) => + openTab(host, "terminal", { + instanceId: + typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, + restoredSessionId: null, + initialFilePath: path, + }), + renameTab, + saveQuickConnectHost, + isFocusedPane, + { + terminalTabs, + targetTerminalTabId, + storageMode: + userPrefs.storageMode === "cloud" + ? "cloud" + : "local", + }, + ), + tabNode, + tab.id, + ); + })} +
+ + {/* Bottom nav bar โ€” mobile only */} +
- {/* Bottom nav bar โ€” mobile only */} - + {/* Right dock โ€” desktop only, holds a second reference panel */} + {!isMobile && rightRailView && !settingsFullscreen && ( +
+
+ + {sidebarTitle(rightRailView)} + + + +
+ + {renderSidebarPanels(rightRailView, false)} + +
+
+ )}
@@ -1937,6 +2935,9 @@ export function AppShell({ isOpen={commandPaletteOpen} setIsOpen={setCommandPaletteOpen} hosts={allHosts} + terminalTabs={terminalTabs} + activeTabId={activeTabId} + onOpenPanel={(view) => handleRailClick(view as RailView)} onOpenTab={(type, label, pendingEvent) => { if ( [ @@ -1947,6 +2948,8 @@ export function AppShell({ ].includes(type) ) { openSingletonTab(type, pendingEvent); + } else if (type === "local-terminal") { + openLocalTerminalTab(); } else if (type === "tmux_monitor") { // --- tmux-monitor --- singleton tab, optionally preselecting a host openSingletonTab( @@ -1966,8 +2969,13 @@ export function AppShell({ + setShowOnboarding(false)} + /> diff --git a/src/ui/api/acme-ssl-api.ts b/src/ui/api/acme-ssl-api.ts index 64bb286..405bf99 100644 --- a/src/ui/api/acme-ssl-api.ts +++ b/src/ui/api/acme-ssl-api.ts @@ -1,6 +1,6 @@ import { authApi, handleApiError } from "@/main-axios"; -export type AcmeChallengeType = "http-webroot" | "dns-cloudflare"; +export type AcmeChallengeType = "http-webroot" | "dns-cloudflare" | "manual"; export type AcmeSettings = { enabled: boolean; @@ -36,7 +36,7 @@ export async function updateAcmeSslSettings( } export async function requestAcmeCertificate(): Promise< - AcmeSettings & { success: boolean } + AcmeSettings & { success: boolean; reloadMessage?: string } > { try { const response = await authApi.post("/users/acme-ssl-request", {}); @@ -45,3 +45,15 @@ export async function requestAcmeCertificate(): Promise< handleApiError(error, "request ACME certificate"); } } + +export async function uploadManualSslCertificate(payload: { + certificate: string; + privateKey: string; +}): Promise { + try { + const response = await authApi.post("/users/manual-ssl-upload", payload); + return response.data; + } catch (error) { + handleApiError(error, "upload manual SSL certificate"); + } +} diff --git a/src/ui/api/admin-user-data-api.ts b/src/ui/api/admin-user-data-api.ts index ecac34e..e6c48dc 100644 --- a/src/ui/api/admin-user-data-api.ts +++ b/src/ui/api/admin-user-data-api.ts @@ -99,7 +99,14 @@ export async function adminDeleteUserHost( export async function adminGetHostPassword( targetUserId: string, hostId: number, - field: "password" | "sudoPassword" | "vncPassword" = "password", + field: + | "password" + | "sudoPassword" + | "rdpPassword" + | "vncPassword" + | "telnetPassword" + | "key" + | "keyPassword" = "password", ): Promise { try { const response = await sshHostApi.get( diff --git a/src/ui/api/ai-api.ts b/src/ui/api/ai-api.ts new file mode 100644 index 0000000..094aca0 --- /dev/null +++ b/src/ui/api/ai-api.ts @@ -0,0 +1,214 @@ +import { authApi, handleApiError } from "@/main-axios"; + +export type AiProviderType = + "ollama" | "anthropic" | "openai" | "gemini" | "openai_compatible"; + +export interface AiProvider { + id: number; + providerType: AiProviderType; + label: string; + baseUrl: string | null; + /** The first few characters only; the key itself never leaves the server. */ + apiKeyPrefix: string | null; + defaultModel: string | null; + enabled: boolean; + createdAt: string; +} + +export interface AiConversation { + id: number; + title: string | null; + providerId: number | null; + model: string | null; + createdAt: string; + updatedAt: string; +} + +export interface AiMessage { + id: number; + conversationId: number; + role: "user" | "assistant" | "tool"; + content: string; + toolCalls: string | null; + createdAt: string; +} + +export interface AiProposal { + id: number; + conversationId: number; + kind: string; + summary: string | null; + payload: string; + status: "pending" | "applied" | "rejected" | "expired"; + resultSummary: string | null; + createdAt: string; +} + +export interface AiStatus { + globallyEnabled: boolean; + enabled: boolean; + allowReadOnlyCommands: boolean; +} + +export async function getAiStatus(): Promise { + try { + return (await authApi.get("/ai/status")).data; + } catch (error) { + throw handleApiError(error, "get AI status"); + } +} + +export async function getAiProviders(): Promise { + try { + return (await authApi.get("/ai/providers")).data.providers; + } catch (error) { + throw handleApiError(error, "list AI providers"); + } +} + +export async function createAiProvider(input: { + providerType: AiProviderType; + label: string; + baseUrl?: string | null; + apiKey?: string | null; + defaultModel?: string | null; +}): Promise { + try { + return (await authApi.post("/ai/providers", input)).data.provider; + } catch (error) { + throw handleApiError(error, "create AI provider"); + } +} + +export async function updateAiProvider( + id: number, + input: Partial<{ + label: string; + baseUrl: string | null; + apiKey: string | null; + defaultModel: string | null; + enabled: boolean; + }>, +): Promise { + try { + return (await authApi.patch(`/ai/providers/${id}`, input)).data.provider; + } catch (error) { + throw handleApiError(error, "update AI provider"); + } +} + +export async function deleteAiProvider(id: number): Promise { + try { + await authApi.delete(`/ai/providers/${id}`); + } catch (error) { + throw handleApiError(error, "delete AI provider"); + } +} + +/** + * Model list for a provider that may not be saved yet, so the add form can + * fill its picker before anything is persisted. + */ +export async function probeAiModels(input: { + providerType: AiProviderType; + baseUrl?: string | null; + apiKey?: string | null; + providerId?: number | null; +}): Promise<{ models: string[]; source: "live" | "fallback" | "none" }> { + try { + return (await authApi.post("/ai/probe-models", input)).data; + } catch (error) { + throw handleApiError(error, "detect models"); + } +} + +export async function getAiProviderModels(id: number): Promise { + try { + return (await authApi.get(`/ai/providers/${id}/models`)).data.models; + } catch (error) { + throw handleApiError(error, "list provider models"); + } +} + +export async function getAiConversations(): Promise { + try { + return (await authApi.get("/ai/conversations")).data.conversations; + } catch (error) { + throw handleApiError(error, "list AI conversations"); + } +} + +export async function getAiConversation(id: number): Promise<{ + conversation: AiConversation; + messages: AiMessage[]; + proposals: AiProposal[]; +}> { + try { + return (await authApi.get(`/ai/conversations/${id}`)).data; + } catch (error) { + throw handleApiError(error, "load AI conversation"); + } +} + +export async function deleteAiConversation(id: number): Promise { + try { + await authApi.delete(`/ai/conversations/${id}`); + } catch (error) { + throw handleApiError(error, "delete AI conversation"); + } +} + +export async function applyAiProposal( + id: number, +): Promise<{ success: boolean; summary: string }> { + try { + return (await authApi.post(`/ai/proposals/${id}/apply`)).data; + } catch (error) { + throw handleApiError(error, "apply AI proposal"); + } +} + +export async function rejectAiProposal(id: number): Promise { + try { + await authApi.post(`/ai/proposals/${id}/reject`); + } catch (error) { + throw handleApiError(error, "reject AI proposal"); + } +} + +// --- admin --- + +export async function getAiGloballyEnabled(): Promise { + try { + return (await authApi.get("/users/ai-enabled")).data.enabled; + } catch (error) { + throw handleApiError(error, "get AI enabled setting"); + } +} + +export async function setAiGloballyEnabled(enabled: boolean): Promise { + try { + return (await authApi.patch("/users/ai-enabled", { enabled })).data.enabled; + } catch (error) { + throw handleApiError(error, "update AI enabled setting"); + } +} + +export async function getAiPrivateEndpoints(): Promise { + try { + return (await authApi.get("/users/ai-private-endpoints")).data.hosts; + } catch (error) { + throw handleApiError(error, "get AI endpoint allowlist"); + } +} + +export async function setAiPrivateEndpoints( + hosts: string[], +): Promise { + try { + return (await authApi.patch("/users/ai-private-endpoints", { hosts })).data + .hosts; + } catch (error) { + throw handleApiError(error, "update AI endpoint allowlist"); + } +} diff --git a/src/ui/api/alerts-api.ts b/src/ui/api/alerts-api.ts index 2b1ef50..6a5bfe9 100644 --- a/src/ui/api/alerts-api.ts +++ b/src/ui/api/alerts-api.ts @@ -4,7 +4,7 @@ export interface NotificationChannel { id: number; userId: string; name: string; - type: "webhook" | "ntfy"; + type: "webhook" | "ntfy" | "discord"; config: string; enabled: boolean; createdAt: string; @@ -69,8 +69,19 @@ export async function getNotificationChannels(): Promise< return res.data; } +export type NotificationChannelPayload = Partial< + Omit +> & { + // When creating/updating the channel the UI may pass a parsed object + // for `config` (e.g., { url, username }) โ€” the backend stores it as + // a JSON string. Accept either a string or any structured object here. + // Use `unknown` to allow the component-local config types to be passed + // without importing them into this module. + config: string | unknown; +}; + export async function createNotificationChannel( - data: Partial, + data: NotificationChannelPayload, ): Promise { const res = await rbacApi.post("/notification-channels", data); return res.data; @@ -78,7 +89,7 @@ export async function createNotificationChannel( export async function updateNotificationChannel( id: number, - data: Partial, + data: NotificationChannelPayload, ): Promise { const res = await rbacApi.put(`/notification-channels/${id}`, data); return res.data; @@ -89,7 +100,11 @@ export async function deleteNotificationChannel(id: number): Promise { } export async function testNotificationChannel(id: number): Promise { - await rbacApi.post(`/notification-channels/${id}/test`); + const res = await rbacApi.post(`/notification-channels/${id}/test`); + const data = res.data as { success?: boolean; error?: string }; + if (data && data.success === false) { + throw new Error(data.error || "Test notification failed"); + } } function mapRule(r: Record): AlertRule { @@ -101,8 +116,7 @@ function mapRule(r: Record): AlertRule { enabled: Boolean(r.enabled), triggerType: (r.trigger_type ?? r.triggerType) as string, thresholdValue: (r.threshold_value ?? r.thresholdValue ?? null) as - | number - | null, + number | null, thresholdDurationSeconds: (r.threshold_duration_seconds ?? r.thresholdDurationSeconds ?? null) as number | null, diff --git a/src/ui/api/audit-log-api.ts b/src/ui/api/audit-log-api.ts index a22c48d..21fca34 100644 --- a/src/ui/api/audit-log-api.ts +++ b/src/ui/api/audit-log-api.ts @@ -1,6 +1,6 @@ import { authApi, handleApiError } from "@/main-axios"; -export interface AuditLog { +export type AuditLog = { id: number; userId: string; username: string; @@ -14,7 +14,7 @@ export interface AuditLog { success: boolean; errorMessage: string | null; timestamp: string; -} +}; export interface AuditLogFilters { page?: number; diff --git a/src/ui/api/automations-api.ts b/src/ui/api/automations-api.ts new file mode 100644 index 0000000..ca3c6bc --- /dev/null +++ b/src/ui/api/automations-api.ts @@ -0,0 +1,143 @@ +import { authApi, handleApiError } from "@/main-axios"; +import type { + AutomationDefinition, + ConcurrencyPolicy, + RunStatus, + StepStatus, + StepType, + TriggerKind, +} from "@/types/automations"; + +export interface AutomationRow { + id: number; + user_id: string; + name: string; + description: string | null; + enabled: number; + definition: AutomationDefinition | null; + definition_version: number; + concurrency_policy: ConcurrencyPolicy; + max_run_seconds: number; + dry_run: number; + last_run_at: string | null; + last_run_status: RunStatus | null; + created_at: string; + updated_at: string; + channels: number[]; + /** Returned once, only when a webhook automation is created. */ + webhookToken?: string; +} + +export interface AutomationRunRow { + id: number; + automation_id: number; + user_id: string; + trigger_type: TriggerKind | "manual"; + trigger_context: string | null; + status: RunStatus; + started_at: string; + finished_at: string | null; + duration_ms: number | null; + error: string | null; + dry_run: number; + parent_run_id: number | null; + automation_name?: string | null; +} + +export interface AutomationRunStepRow { + id: number; + run_id: number; + step_index: number; + step_id: string; + step_type: StepType; + status: StepStatus; + started_at: string; + finished_at: string | null; + output: string | null; + error: string | null; + truncated: number; +} + +export interface AutomationRunOutcome { + runId: number | null; + status: RunStatus; + error?: string; +} + +export interface AutomationInput { + name: string; + description?: string | null; + enabled?: boolean; + definition: AutomationDefinition; + concurrencyPolicy?: ConcurrencyPolicy; + channels?: number[]; +} + +export async function listAutomations(): Promise { + try { + return (await authApi.get("/automations")).data; + } catch (error) { + throw handleApiError(error, "fetch automations"); + } +} + +export async function createAutomation( + input: AutomationInput, +): Promise { + try { + return (await authApi.post("/automations", input)).data; + } catch (error) { + throw handleApiError(error, "create automation"); + } +} + +export async function updateAutomation( + id: number, + input: Partial, +): Promise { + try { + return (await authApi.put(`/automations/${id}`, input)).data; + } catch (error) { + throw handleApiError(error, "update automation"); + } +} + +export async function deleteAutomation(id: number): Promise { + try { + await authApi.delete(`/automations/${id}`); + } catch (error) { + throw handleApiError(error, "delete automation"); + } +} + +export async function runAutomation( + id: number, + options: { dryRun?: boolean } = {}, +): Promise { + try { + return (await authApi.post(`/automations/${id}/run`, options)).data; + } catch (error) { + throw handleApiError(error, "run automation"); + } +} + +export async function listAutomationRuns( + options: { automationId?: number; limit?: number; offset?: number } = {}, +): Promise { + try { + return (await authApi.get("/automations/runs/history", { params: options })) + .data; + } catch (error) { + throw handleApiError(error, "fetch automation runs"); + } +} + +export async function listAutomationRunSteps( + runId: number, +): Promise { + try { + return (await authApi.get(`/automations/runs/${runId}/steps`)).data; + } catch (error) { + throw handleApiError(error, "fetch run steps"); + } +} diff --git a/src/ui/api/credential-sidebar-preferences-api.ts b/src/ui/api/credential-sidebar-preferences-api.ts new file mode 100644 index 0000000..eab0193 --- /dev/null +++ b/src/ui/api/credential-sidebar-preferences-api.ts @@ -0,0 +1,29 @@ +import { authApi, handleApiError } from "@/main-axios"; +import { + sanitizeCredentialSidebarPreferences, + type CredentialSidebarPreferences, +} from "@/types/credential-sidebar-preferences"; + +// CREDENTIAL SIDEBAR PREFERENCES API +// ============================================================================ + +export async function getCredentialSidebarPreferences(): Promise { + try { + const response = await authApi.get("/credential-sidebar/preferences"); + return sanitizeCredentialSidebarPreferences(response.data?.preferences); + } catch (error) { + handleApiError(error, "fetch credential sidebar preferences"); + throw error; + } +} + +export async function saveCredentialSidebarPreferences( + preferences: CredentialSidebarPreferences, +): Promise { + try { + await authApi.put("/credential-sidebar/preferences", preferences); + } catch (error) { + handleApiError(error, "save credential sidebar preferences"); + throw error; + } +} diff --git a/src/ui/api/credentials-api.ts b/src/ui/api/credentials-api.ts index 7c0f1e7..1e8336f 100644 --- a/src/ui/api/credentials-api.ts +++ b/src/ui/api/credentials-api.ts @@ -1,6 +1,11 @@ import { authApi, handleApiError, sshHostApi } from "@/main-axios"; import type { SSHFolder } from "@/types/index"; import { sshLogger } from "@/lib/frontend-logger"; +import { + getCachedSSHFolders, + invalidateSSHFoldersCache, + invalidateHostsAndStatusCaches, +} from "@/lib/hosts-request-cache"; export async function getCredentials(): Promise< Record[] | Record @@ -96,7 +101,14 @@ export async function getSSHHostWithCredentials( export async function getHostPassword( hostId: number, - field: "password" | "sudoPassword" | "vncPassword" = "password", + field: + | "password" + | "sudoPassword" + | "rdpPassword" + | "vncPassword" + | "telnetPassword" + | "key" + | "keyPassword" = "password", ): Promise { try { const response = await sshHostApi.get( @@ -167,6 +179,7 @@ export async function renameFolder( oldName, newName, }); + invalidateSSHFoldersCache(); return response.data; } catch (error) { handleApiError(error, "rename folder"); @@ -179,14 +192,17 @@ export async function getSSHFolders(): Promise { operation: "fetch_ssh_folders", }); - const response = await authApi.get("/host/folders"); + const folders = await getCachedSSHFolders(async () => { + const response = await authApi.get("/host/folders"); + return response.data; + }); sshLogger.success("SSH folders fetched successfully", { operation: "fetch_ssh_folders", - count: response.data.length, + count: folders.length, }); - return response.data; + return folders; } catch (error) { sshLogger.error("Failed to fetch SSH folders", error, { operation: "fetch_ssh_folders", @@ -200,6 +216,7 @@ export async function updateFolderMetadata( name: string, color?: string, icon?: string, + credentialId?: number | null, ): Promise { try { sshLogger.info("Updating folder metadata", { @@ -207,14 +224,18 @@ export async function updateFolderMetadata( name, color, icon, + credentialId, }); await authApi.put("/host/folders/metadata", { name, color, icon, + credentialId, }); + invalidateSSHFoldersCache(); + sshLogger.success("Folder metadata updated successfully", { operation: "update_folder_metadata", name, @@ -229,6 +250,21 @@ export async function updateFolderMetadata( } } +export async function reorderFolders( + positions: { name: string; sortOrder: number }[], +): Promise<{ updated: number }> { + try { + const response = await authApi.put("/host/folders/reorder", { + positions, + }); + invalidateSSHFoldersCache(); + return response.data; + } catch (error) { + handleApiError(error, "reorder folders"); + throw error; + } +} + export async function deleteAllHostsInFolder( folderName: string, ): Promise<{ deletedCount: number }> { @@ -242,6 +278,8 @@ export async function deleteAllHostsInFolder( `/host/folders/${encodeURIComponent(folderName)}/hosts`, ); + invalidateHostsAndStatusCaches(); + sshLogger.success("All hosts in folder deleted successfully", { operation: "delete_folder_hosts", folderName, @@ -268,12 +306,27 @@ export async function renameCredentialFolder( oldName, newName, }); + invalidateSSHFoldersCache(); return response.data; } catch (error) { throw handleApiError(error, "rename credential folder"); } } +export async function reorderCredentials( + positions: { id: number; sortOrder: number }[], +): Promise<{ updated: number }> { + try { + const response = await authApi.put("/credentials/reorder", { + positions, + }); + return response.data; + } catch (error) { + handleApiError(error, "reorder credentials"); + throw error; + } +} + export async function detectKeyType( privateKey: string, keyPassword?: string, @@ -319,10 +372,16 @@ export async function validateKeyPair( } } +export interface GeneratedPublicKey { + success?: boolean; + publicKey?: string; + error?: string; +} + export async function generatePublicKeyFromPrivate( privateKey: string, keyPassword?: string, -): Promise> { +): Promise { try { const response = await authApi.post("/credentials/generate-public-key", { privateKey, @@ -334,11 +393,23 @@ export async function generatePublicKeyFromPrivate( } } +export interface GeneratedKeyPair { + success: boolean; + privateKey?: string; + publicKey?: string; + keyType?: string; + format?: string; + algorithm?: string; + keySize?: number; + curve?: string; + error?: string; +} + export async function generateKeyPair( keyType: "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256", keySize?: number, passphrase?: string, -): Promise> { +): Promise { try { const response = await authApi.post("/credentials/generate-key-pair", { keyType, diff --git a/src/ui/api/file-manager-data-api.ts b/src/ui/api/file-manager-data-api.ts index 3aeafb4..5137853 100644 --- a/src/ui/api/file-manager-data-api.ts +++ b/src/ui/api/file-manager-data-api.ts @@ -1,11 +1,20 @@ import { authApi, handleApiError } from "@/main-axios"; +/** Row shape shared by the recent / pinned / shortcut list endpoints. */ +export interface FileManagerEntry { + id: number; + name: string; + path: string; + lastOpened?: string; + [key: string]: unknown; +} + // FILE MANAGER DATA // ============================================================================ export async function getRecentFiles( hostId: number, -): Promise> { +): Promise { try { const response = await authApi.get("/host/file_manager/recent", { params: { hostId }, @@ -52,7 +61,7 @@ export async function removeRecentFile( export async function getPinnedFiles( hostId: number, -): Promise> { +): Promise { try { const response = await authApi.get("/host/file_manager/pinned", { params: { hostId }, @@ -99,7 +108,7 @@ export async function removePinnedFile( export async function getFolderShortcuts( hostId: number, -): Promise> { +): Promise { try { const response = await authApi.get("/host/file_manager/shortcuts", { params: { hostId }, diff --git a/src/ui/api/fleets-api.ts b/src/ui/api/fleets-api.ts new file mode 100644 index 0000000..c54cc66 --- /dev/null +++ b/src/ui/api/fleets-api.ts @@ -0,0 +1,247 @@ +import { authApi, handleApiError } from "@/main-axios"; + +export interface FleetRow { + id: number; + userId: string; + name: string; + description: string | null; + color: string | null; + icon: string | null; + tagRules: string[]; + syncId: string | null; + createdAt: string; + updatedAt: string; + memberCount: number; +} + +export interface FleetMemberRow { + id: number; + name: string; + ip: string; + tags: string[]; + static: boolean; + permissionLevel: "connect" | "view" | "edit" | "manage" | null; +} + +export interface FleetHostResult { + hostId: number; + hostName: string; + success: boolean; + output?: string; + error?: string; +} + +export interface FleetInventoryRecord { + id: number; + hostId: number; + userId: string; + osPrettyName: string | null; + kernel: string | null; + architecture: string | null; + hostname: string | null; + uptimeSeconds: number | null; + ip: string | null; + packageManager: string | null; + collectedAt: string; +} + +export interface FleetInventoryEntry { + hostId: number; + hostName: string; + inventory: FleetInventoryRecord | null; +} + +export type FleetPackageAction = "install" | "remove" | "upgrade-all"; + +export async function listFleets(): Promise { + try { + const response = await authApi.get("/fleets"); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch fleets"); + } +} + +export async function createFleet(fleetData: { + name: string; + description?: string | null; + color?: string | null; + icon?: string | null; + tagRules?: string[]; +}): Promise { + try { + const response = await authApi.post("/fleets", fleetData); + return response.data; + } catch (error) { + throw handleApiError(error, "create fleet"); + } +} + +export async function updateFleet( + fleetId: number, + fleetData: { + name?: string; + description?: string | null; + color?: string | null; + icon?: string | null; + tagRules?: string[]; + }, +): Promise { + try { + const response = await authApi.patch(`/fleets/${fleetId}`, fleetData); + return response.data; + } catch (error) { + throw handleApiError(error, "update fleet"); + } +} + +export async function deleteFleet( + fleetId: number, +): Promise<{ success: boolean }> { + try { + const response = await authApi.delete(`/fleets/${fleetId}`); + return response.data; + } catch (error) { + throw handleApiError(error, "delete fleet"); + } +} + +export async function getFleetMembers( + fleetId: number, +): Promise { + try { + const response = await authApi.get(`/fleets/${fleetId}/members`); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch fleet members"); + } +} + +export async function addFleetMember( + fleetId: number, + hostId: number, +): Promise<{ success: boolean }> { + try { + const response = await authApi.post(`/fleets/${fleetId}/members`, { + hostId, + }); + return response.data; + } catch (error) { + throw handleApiError(error, "add fleet member"); + } +} + +export async function removeFleetMember( + fleetId: number, + hostId: number, +): Promise<{ success: boolean }> { + try { + const response = await authApi.delete( + `/fleets/${fleetId}/members/${hostId}`, + ); + return response.data; + } catch (error) { + throw handleApiError(error, "remove fleet member"); + } +} + +export async function runFleetCommand( + fleetId: number, + command: string, + inputValues?: Record, +): Promise<{ results: FleetHostResult[] }> { + try { + const response = await authApi.post(`/fleets/${fleetId}/execute`, { + command, + ...(inputValues ? { inputValues } : {}), + }); + return response.data; + } catch (error) { + throw handleApiError(error, "run fleet command"); + } +} + +export async function pushFleetFile( + fleetId: number, + file: File, + remotePath: string, +): Promise<{ results: FleetHostResult[] }> { + try { + const form = new FormData(); + form.append("file", file); + form.append("remotePath", remotePath); + const response = await authApi.post( + `/fleets/${fleetId}/transfer/push`, + form, + { headers: { "Content-Type": "multipart/form-data" } }, + ); + return response.data; + } catch (error) { + throw handleApiError(error, "push file to fleet"); + } +} + +export async function pullFleetFile( + fleetId: number, + remotePath: string, +): Promise<{ results: FleetHostResult[]; blob: Blob; fileName: string }> { + try { + const response = await authApi.post( + `/fleets/${fleetId}/transfer/pull`, + { remotePath }, + { responseType: "blob" }, + ); + + const resultsHeader = response.headers["x-fleet-transfer-results"]; + const results: FleetHostResult[] = resultsHeader + ? JSON.parse(atob(resultsHeader)) + : []; + + const disposition = response.headers["content-disposition"] as + string | undefined; + const match = disposition?.match(/filename="([^"]+)"/); + const fileName = match?.[1] ?? "fleet-transfer.zip"; + + return { results, blob: response.data, fileName }; + } catch (error) { + throw handleApiError(error, "pull file from fleet"); + } +} + +export async function getFleetInventory( + fleetId: number, +): Promise { + try { + const response = await authApi.get(`/fleets/${fleetId}/inventory`); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch fleet inventory"); + } +} + +export async function refreshFleetInventory( + fleetId: number, +): Promise<{ results: FleetHostResult[] }> { + try { + const response = await authApi.post(`/fleets/${fleetId}/inventory`); + return response.data; + } catch (error) { + throw handleApiError(error, "refresh fleet inventory"); + } +} + +export async function runFleetPackageAction( + fleetId: number, + action: FleetPackageAction, + packageName?: string, +): Promise<{ results: FleetHostResult[] }> { + try { + const response = await authApi.post(`/fleets/${fleetId}/packages`, { + action, + ...(packageName ? { package: packageName } : {}), + }); + return response.data; + } catch (error) { + throw handleApiError(error, "run fleet package action"); + } +} diff --git a/src/ui/api/guacamole-api.ts b/src/ui/api/guacamole-api.ts index abb59e6..419173a 100644 --- a/src/ui/api/guacamole-api.ts +++ b/src/ui/api/guacamole-api.ts @@ -1,4 +1,21 @@ -import { authApi, handleApiError } from "@/main-axios"; +import { + authApi, + getRemoteGuacamoleApi, + handleApiError, + isElectron, +} from "@/main-axios"; +import type { AxiosInstance } from "axios"; +import type { GuacamoleConfig } from "@/types/guacamole-config"; + +/** + * The embedded desktop backend does not bundle guacd, which is why + * resolveConnectionOrigin() pins RDP/VNC/Telnet to "remote". These calls have to + * follow: asking the embedded backend reports the guacd *it* cannot reach, + * rather than the one on the connected server that serves the session. + */ +function guacamoleApi(): AxiosInstance { + return isElectron() ? getRemoteGuacamoleApi() : authApi; +} export interface GuacamoleTokenRequest { protocol: "rdp" | "vnc" | "telnet"; @@ -9,75 +26,30 @@ export interface GuacamoleTokenRequest { domain?: string; security?: string; ignoreCert?: boolean; - guacamoleConfig?: { - colorDepth?: number; - width?: number; - height?: number; - dpi?: number; - resizeMethod?: string; - forceLossless?: boolean; - disableAudio?: boolean; - enableAudioInput?: boolean; - enableWallpaper?: boolean; - enableTheming?: boolean; - enableFontSmoothing?: boolean; - enableFullWindowDrag?: boolean; - enableDesktopComposition?: boolean; - enableMenuAnimations?: boolean; - disableBitmapCaching?: boolean; - disableOffscreenCaching?: boolean; - disableGlyphCaching?: boolean; - disableGfx?: boolean; - enablePrinting?: boolean; - printerName?: string; - enableDrive?: boolean; - driveName?: string; - drivePath?: string; - createDrivePath?: boolean; - disableDownload?: boolean; - disableUpload?: boolean; - enableTouch?: boolean; - clientName?: string; - console?: boolean; - initialProgram?: string; - serverLayout?: string; - timezone?: string; - gatewayHostname?: string; - gatewayPort?: number; - gatewayUsername?: string; - gatewayPassword?: string; - gatewayDomain?: string; - remoteApp?: string; - remoteAppDir?: string; - remoteAppArgs?: string; - normalizeClipboard?: string; - disableCopy?: boolean; - disablePaste?: boolean; - cursor?: string; - swapRedBlue?: boolean; - readOnly?: boolean; - recordingPath?: string; - recordingName?: string; - createRecordingPath?: boolean; - recordingExcludeOutput?: boolean; - recordingExcludeMouse?: boolean; - recordingIncludeKeys?: boolean; - wolSendPacket?: boolean; - wolMacAddr?: string; - wolBroadcastAddr?: string; - wolUdpPort?: number; - wolWaitTime?: number; - }; + guacamoleConfig?: GuacamoleConfig; } export interface GuacamoleTokenResponse { token: string; + guacamoleConnectionId?: string | null; } type GuacamoleConfigSource = { guacamoleConfig?: string | Record | null; }; +export function parseGuacamoleConfig( + config?: string | GuacamoleConfig | null, +): GuacamoleConfig { + if (!config) return {}; + if (typeof config !== "string") return config; + try { + return JSON.parse(config) as GuacamoleConfig; + } catch { + return {}; + } +} + export function getGuacamoleDpi( source?: GuacamoleConfigSource, ): number | undefined { @@ -188,7 +160,7 @@ export async function getGuacamoleToken( try { const guacParams = toGuacamoleParams(request.guacamoleConfig); - const response = await authApi.post("/guacamole/token", { + const response = await guacamoleApi().post("/guacamole/token", { type: request.protocol, hostname: request.hostname, port: request.port, @@ -208,11 +180,27 @@ export async function getGuacamoleToken( export async function getGuacamoleTokenFromHost( hostId: number, protocol?: "rdp" | "vnc" | "telnet", + promptedCredentials?: { + username?: string; + password?: string; + domain?: string; + }, ): Promise { try { - const response = await authApi.post( + const response = await guacamoleApi().post( `/guacamole/connect-host/${hostId}`, - protocol ? { protocol } : {}, + { + ...(protocol ? { protocol } : {}), + ...(promptedCredentials?.username + ? { promptedUsername: promptedCredentials.username } + : {}), + ...(promptedCredentials?.password + ? { promptedPassword: promptedCredentials.password } + : {}), + ...(promptedCredentials + ? { promptedDomain: promptedCredentials.domain ?? "" } + : {}), + }, ); return response.data; } catch (error) { @@ -223,6 +211,6 @@ export async function getGuacamoleTokenFromHost( export async function getGuacdStatus(): Promise<{ guacd: { status: string }; }> { - const response = await authApi.get("/guacamole/status"); + const response = await guacamoleApi().get("/guacamole/status"); return response.data; } diff --git a/src/ui/api/homepage-api.ts b/src/ui/api/homepage-api.ts index e47f0f1..cb6f3f6 100644 --- a/src/ui/api/homepage-api.ts +++ b/src/ui/api/homepage-api.ts @@ -70,12 +70,3 @@ export async function saveHomepageLayout( throw handleApiError(error, "save homepage layout"); } } - -export function getHomepageFaviconUrl(url: string): string { - try { - const base = (homepageApi.defaults.baseURL ?? "").replace(/\/$/, ""); - return `${base}/favicon?url=${encodeURIComponent(url)}`; - } catch { - return ""; - } -} diff --git a/src/ui/api/host-metrics-api.ts b/src/ui/api/host-metrics-api.ts index f45b89e..f7e06ee 100644 --- a/src/ui/api/host-metrics-api.ts +++ b/src/ui/api/host-metrics-api.ts @@ -1,6 +1,15 @@ import { handleApiError, statsApi } from "@/main-axios"; import type { HostMetricsLayout } from "@/types/host-metrics"; +// Every function below is keyed by a host's numeric database id, and the +// receiving backend must own that host in its own database -- a synced +// host has a different numeric id on each side (only its syncId matches +// across them). These calls always target the embedded local backend; see +// getAllServerStatuses in host-metrics-status-api.ts for the one metrics +// call that IS safely merged across local + remote (a process-local, +// in-memory aggregate keyed by whichever host ids that process happens to +// know about, not a per-host lookup). + export interface MetricsHistoryRow { ts: string; cpu_percent: number | null; diff --git a/src/ui/api/host-metrics-status-api.ts b/src/ui/api/host-metrics-status-api.ts index 0a6eebe..bd4da38 100644 --- a/src/ui/api/host-metrics-status-api.ts +++ b/src/ui/api/host-metrics-status-api.ts @@ -1,7 +1,34 @@ import axios, { type AxiosRequestConfig } from "axios"; -import { handleApiError, statsApi } from "@/main-axios"; +import { + handleApiError, + statsApi, + getRemoteStatsApi, + isElectron, + sshHostApi, +} from "@/main-axios"; import type { ServerMetrics, ServerStatus } from "@/main-axios"; import { getCachedServerStatuses } from "@/lib/hosts-request-cache"; +import { resolveConnectionOrigin } from "@/lib/connection-origin"; +import type { SSHHost } from "@/types/index"; +import { createKeyedRequestCache } from "@/lib/keyed-request-cache"; + +// Metrics collection/viewer registration below (startMetricsPolling, +// registerMetricsViewer, etc.) is NOT origin-routed: the backend that +// receives the call must own the target host by numeric database id, and a +// synced host has a different numeric id in each database (only its +// syncId matches across them). Only the aggregate status read is merged +// across local + remote, same as tunnel status. +async function isRemoteSyncConnected(): Promise { + if (!isElectron()) return false; + try { + const config = (await window.electronAPI?.invoke?.( + "get-remote-sync-config", + )) as { serverUrl?: string } | null; + return !!config?.serverUrl; + } catch { + return false; + } +} type ApiConnectionLog = { type: "info" | "success" | "warning" | "error"; @@ -27,6 +54,8 @@ type ConnectErrorResponse = { // SERVER STATISTICS // ============================================================================ +const metricsCache = createKeyedRequestCache(1_500, 100); + /** * Progressive retry schedule for the background /status poll. * @@ -76,6 +105,27 @@ export async function getAllServerStatuses(): Promise< > { return getCachedServerStatuses(async () => { let lastError: unknown = null; + let localStatuses: Record = {}; + let localHostIds: number[] | null = null; + + if (isElectron()) { + try { + const response = await sshHostApi.get("/db/host"); + const defaultOrigin = await resolveConnectionOrigin({ + connectionType: "ssh", + connectionOrigin: null, + }); + localHostIds = (response.data || []) + .filter( + (host) => (host.connectionOrigin ?? defaultOrigin) === "local", + ) + .map((host) => host.id); + } catch { + // A host-list failure must not start local probes for hosts whose + // configured origin may be remote. + localHostIds = []; + } + } for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) { const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i]; @@ -84,12 +134,17 @@ export async function getAllServerStatuses(): Promise< try { const response = await statsApi.get("/status", { timeout: timeoutMs, + ...(localHostIds === null + ? {} + : { params: { hostIds: localHostIds.join(",") } }), // Silence per-attempt interceptor logging & health-monitor side // effects on all attempts except the final one, so background // blips don't look like real outages. __silentRetry: !isFinalAttempt, } as AxiosRequestConfig & { __silentRetry?: boolean }); - return response.data || {}; + localStatuses = response.data || {}; + lastError = null; + break; } catch (error) { lastError = error; if (!isTransientStatusError(error)) { @@ -102,8 +157,24 @@ export async function getAllServerStatuses(): Promise< } } - handleApiError(lastError, "fetch server statuses"); - return {}; + if (lastError) { + handleApiError(lastError, "fetch server statuses"); + return {}; + } + + if (await isRemoteSyncConnected()) { + try { + const remoteResult = await getRemoteStatsApi().get("/status", { + timeout: 8000, + __silentRetry: true, + } as AxiosRequestConfig & { __silentRetry?: boolean }); + return { ...localStatuses, ...(remoteResult.data || {}) }; + } catch { + // remote unreachable this tick -- fall back to local-only statuses + } + } + + return localStatuses; }); } @@ -120,25 +191,24 @@ export async function getServerStatusById(id: number): Promise { export async function getServerMetricsById( id: number, ): Promise { - try { - const response = await statsApi.get(`/metrics/${id}`, { - // Treat 404 as an expected "no metrics yet / disabled" signal rather - // than an error so we don't spam warn logs on the client. - validateStatus: (status) => status === 200 || status === 404, - }); - if (response.status === 404) { - return null; + return metricsCache.get(String(id), async () => { + try { + const response = await statsApi.get(`/metrics/${id}`, { + // Treat 404 as an expected "no metrics yet / disabled" signal rather + // than an error so we don't spam warn logs on the client. + validateStatus: (status) => status === 200 || status === 404, + }); + if (response.status === 404) return null; + return response.data; + } catch (error) { + // If a 404 still slips through (e.g. intercepted before reaching here), + // swallow it quietly; everything else still flows through handleApiError. + if (axios.isAxiosError(error) && error.response?.status === 404) { + return null; + } + handleApiError(error, "fetch server metrics"); } - return response.data; - } catch (error) { - // If a 404 still slips through (e.g. intercepted before reaching here), - // swallow it quietly; everything else still flows through handleApiError. - if (axios.isAxiosError(error) && error.response?.status === 404) { - return null; - } - handleApiError(error, "fetch server metrics"); - throw error; - } + }); } export async function startMetricsPolling(hostId: number): Promise<{ @@ -151,6 +221,7 @@ export async function startMetricsPolling(hostId: number): Promise<{ }> { try { const response = await statsApi.post(`/metrics/start/${hostId}`); + metricsCache.invalidate(String(hostId)); return response.data; } catch (error: unknown) { if ( @@ -204,6 +275,7 @@ export async function registerMetricsViewer(hostId: number): Promise<{ const response = await statsApi.post("/metrics/register-viewer", { hostId, }); + metricsCache.invalidate(String(hostId)); return response.data; } catch (error) { handleApiError(error, "register metrics viewer"); @@ -238,6 +310,7 @@ export async function submitMetricsTOTP( sessionId, totpCode, }); + metricsCache.invalidate(); return response.data; } catch (error) { handleApiError(error, "submit metrics TOTP"); @@ -258,6 +331,7 @@ export async function notifyHostCreatedOrUpdated( ): Promise { try { await statsApi.post("/host-updated", { hostId }); + metricsCache.invalidate(String(hostId)); } catch (error) { console.warn("Failed to notify stats server of host update:", error); } diff --git a/src/ui/api/host-sidebar-preferences-api.ts b/src/ui/api/host-sidebar-preferences-api.ts new file mode 100644 index 0000000..4d670b4 --- /dev/null +++ b/src/ui/api/host-sidebar-preferences-api.ts @@ -0,0 +1,29 @@ +import { authApi, handleApiError } from "@/main-axios"; +import { + sanitizeHostSidebarPreferences, + type HostSidebarPreferences, +} from "@/types/host-sidebar-preferences"; + +// HOST SIDEBAR PREFERENCES API +// ============================================================================ + +export async function getHostSidebarPreferences(): Promise { + try { + const response = await authApi.get("/host-sidebar/preferences"); + return sanitizeHostSidebarPreferences(response.data?.preferences); + } catch (error) { + handleApiError(error, "fetch host sidebar preferences"); + throw error; + } +} + +export async function saveHostSidebarPreferences( + preferences: HostSidebarPreferences, +): Promise { + try { + await authApi.put("/host-sidebar/preferences", preferences); + } catch (error) { + handleApiError(error, "save host sidebar preferences"); + throw error; + } +} diff --git a/src/ui/api/open-tabs-api.ts b/src/ui/api/open-tabs-api.ts index e9eb5a7..e0139bc 100644 --- a/src/ui/api/open-tabs-api.ts +++ b/src/ui/api/open-tabs-api.ts @@ -1,5 +1,7 @@ import { authApi } from "@/main-axios"; import { createTtlRequestCache } from "@/lib/ttl-request-cache"; +import type { TerminalTheme } from "@/lib/terminal-themes"; +import type { CustomKeybinding } from "@/types/keybindings"; // OPEN TABS API // ============================================================================ @@ -41,6 +43,10 @@ export interface ActiveSessionInfo { tabInstanceId: string | null; isConnected: boolean; createdAt: number; + isOwnSession: boolean; + sharedByUsername: string | null; + permissionLevel: string | null; + shareId: string | null; } const activeSessionsCache = createTtlRequestCache(2_000); @@ -61,7 +67,7 @@ export async function deleteOpenTab(instanceId: string): Promise { export async function patchOpenTab( instanceId: string, updates: Partial< - Pick + Pick >, ): Promise { await authApi.patch(`/open-tabs/${instanceId}`, updates); @@ -82,6 +88,12 @@ export async function getActiveSessions(): Promise { // USER PREFERENCES API // ============================================================================ +export interface SavedCustomTheme { + id: string; + name: string; + colors: TerminalTheme["colors"]; +} + export interface UserPreferences { reopenTabsOnLogin: boolean; theme?: string | null; @@ -100,8 +112,37 @@ export interface UserPreferences { disableUpdateCheck?: boolean | null; confirmTabClose?: boolean | null; hiddenRailTabs?: string | null; + aiAssistantEnabled?: boolean | null; + aiReadOnlyCommands?: boolean | null; compactHostView?: boolean | null; statusColorScheme?: string | null; + customThemes?: string | null; + customKeybindings?: string | null; + terminalDefaults?: string | null; + rdpDefaults?: string | null; + terminalMacros?: string | null; +} + +export function parseCustomThemes(raw?: string | null): SavedCustomTheme[] { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +export function parseCustomKeybindings( + raw?: string | null, +): CustomKeybinding[] { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } } export async function getUserPreferences(): Promise { diff --git a/src/ui/api/proxmox-stats-api.ts b/src/ui/api/proxmox-stats-api.ts new file mode 100644 index 0000000..0b45bbd --- /dev/null +++ b/src/ui/api/proxmox-stats-api.ts @@ -0,0 +1,93 @@ +import axios from "axios"; +import { handleApiError, statsApi } from "@/main-axios"; +import type { ProxmoxStatsSnapshot } from "@/types/proxmox"; + +// Every function below is keyed by a host's numeric database id, and the +// receiving backend must own that host in its own database -- same caveat as +// host-metrics-api.ts / host-metrics-status-api.ts. All routes live under the +// `/proxmox-stats/*` prefix on the stats app (port 30005). + +export async function getProxmoxStats( + hostId: number, +): Promise { + try { + const response = await statsApi.get(`/proxmox-stats/${hostId}`, { + // Treat 404 as an expected "no stats yet / disabled" signal rather than + // an error so we don't spam warn logs on the client. + validateStatus: (status) => status === 200 || status === 404, + }); + if (response.status === 404) { + return null; + } + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + return null; + } + handleApiError(error, "fetch proxmox stats"); + throw error; + } +} + +export async function startProxmoxStatsPolling(hostId: number): Promise<{ + success: boolean; + viewerSessionId?: string; + status?: string; + error?: string; +}> { + try { + const response = await statsApi.post(`/proxmox-stats/start/${hostId}`); + return response.data; + } catch (error) { + handleApiError(error, "start proxmox stats polling"); + throw error; + } +} + +export async function stopProxmoxStatsPolling( + hostId: number, + viewerSessionId?: string, +): Promise { + try { + await statsApi.post(`/proxmox-stats/stop/${hostId}`, { viewerSessionId }); + } catch (error) { + handleApiError(error, "stop proxmox stats polling"); + throw error; + } +} + +export async function sendProxmoxStatsHeartbeat( + viewerSessionId: string, +): Promise { + try { + await statsApi.post("/proxmox-stats/heartbeat", { viewerSessionId }); + } catch (error) { + handleApiError(error, "send proxmox stats heartbeat"); + throw error; + } +} + +export interface ProxmoxStatsHistoryRow { + ts: string; + cpu_percent: number | null; + mem_percent: number | null; + disk_percent: number | null; + net_rx_bytes: number | null; + net_tx_bytes: number | null; +} + +export interface ProxmoxStatsHistoryResponse { + rows: ProxmoxStatsHistoryRow[]; + fromTs: string; + toTs: string; +} + +export async function getProxmoxStatsHistory( + hostId: number, + opts: { range?: string; from?: string; to?: string }, +): Promise { + const res = await statsApi.get(`/proxmox-stats/history/${hostId}`, { + params: opts, + }); + return res.data as ProxmoxStatsHistoryResponse; +} diff --git a/src/ui/api/rbac-api.ts b/src/ui/api/rbac-api.ts index 63f2553..5f78b3a 100644 --- a/src/ui/api/rbac-api.ts +++ b/src/ui/api/rbac-api.ts @@ -1,9 +1,30 @@ -import { handleApiError, rbacApi } from "@/main-axios"; -import type { AccessRecord, Role, UserRole } from "@/main-axios"; +import { + handleApiError, + rbacApi, + type AccessRecord, + type Role, + type UserRole, +} from "@/main-axios"; +import type { AuthOverrideProtocol } from "@/types/auth-protocols"; +import { + getConnectedRemoteApi, + resolveRemoteHostId, +} from "@/lib/remote-server-api"; + +async function getSharingTarget(hostId: number, syncId?: string | null) { + const api = await getConnectedRemoteApi(); + if (!api || !syncId) return { api: rbacApi, hostId }; + const remoteHostId = await resolveRemoteHostId(syncId); + if (remoteHostId === null) { + throw new Error("The synced host does not exist on the remote server"); + } + return { api, hostId: remoteHostId }; +} export async function getRoles(): Promise<{ roles: Role[] }> { try { - const response = await rbacApi.get("/rbac/roles"); + const api = (await getConnectedRemoteApi()) ?? rbacApi; + const response = await api.get("/rbac/roles"); return response.data; } catch (error) { throw handleApiError(error, "fetch roles"); @@ -103,6 +124,7 @@ export async function shareHost( permissionLevel: SharePermissionLevel; durationHours?: number; }, + syncId?: string | null, ): Promise<{ success: boolean; expiresAt: string | null; @@ -114,8 +136,9 @@ export async function shareHost( }>; }> { try { - const response = await rbacApi.post( - `/rbac/host/${hostId}/share`, + const target = await getSharingTarget(hostId, syncId); + const response = await target.api.post( + `/rbac/host/${target.hostId}/share`, shareData, ); return response.data; @@ -124,6 +147,32 @@ export async function shareHost( } } +export async function shareFolder( + folder: string, + shareData: { + targets: ShareTarget[]; + permissionLevel: SharePermissionLevel; + durationHours?: number; + }, +): Promise<{ + success: boolean; + expiresAt: string | null; + hostsShared: number; + hostsTotal: number; + hostResults: Array<{ hostId: number; shared: boolean; reason?: string }>; +}> { + try { + const api = (await getConnectedRemoteApi()) ?? rbacApi; + const response = await api.post("/rbac/folder/share", { + folder, + ...shareData, + }); + return response.data; + } catch (error) { + throw handleApiError(error, "share folder"); + } +} + export async function updateHostAccess( hostId: number, accessId: number, @@ -131,10 +180,12 @@ export async function updateHostAccess( permissionLevel?: SharePermissionLevel; durationHours?: number | null; }, + syncId?: string | null, ): Promise<{ success: boolean; expiresAt: string | null }> { try { - const response = await rbacApi.patch( - `/rbac/host/${hostId}/access/${accessId}`, + const target = await getSharingTarget(hostId, syncId); + const response = await target.api.patch( + `/rbac/host/${target.hostId}/access/${accessId}`, update, ); return response.data; @@ -145,9 +196,11 @@ export async function updateHostAccess( export async function getHostAccess( hostId: number, + syncId?: string | null, ): Promise<{ accessList: AccessRecord[]; isOwner?: boolean }> { try { - const response = await rbacApi.get(`/rbac/host/${hostId}/access`); + const target = await getSharingTarget(hostId, syncId); + const response = await target.api.get(`/rbac/host/${target.hostId}/access`); return response.data; } catch (error) { throw handleApiError(error, "fetch host access"); @@ -186,7 +239,8 @@ export async function getSharedHosts(): Promise<{ }>; }> { try { - const response = await rbacApi.get("/rbac/shared-hosts"); + const api = (await getConnectedRemoteApi()) ?? rbacApi; + const response = await api.get("/rbac/shared-hosts"); return response.data; } catch (error) { throw handleApiError(error, "fetch shared hosts"); @@ -196,10 +250,12 @@ export async function getSharedHosts(): Promise<{ export async function revokeHostAccess( hostId: number, accessId: number, + syncId?: string | null, ): Promise<{ success: boolean }> { try { - const response = await rbacApi.delete( - `/rbac/host/${hostId}/access/${accessId}`, + const target = await getSharingTarget(hostId, syncId); + const response = await target.api.delete( + `/rbac/host/${target.hostId}/access/${accessId}`, ); return response.data; } catch (error) { @@ -207,6 +263,40 @@ export async function revokeHostAccess( } } +export async function getHostAuthOverride( + hostId: number, + protocol: AuthOverrideProtocol, +): Promise<{ protocol: AuthOverrideProtocol; credentialId: number | null }> { + try { + const response = await rbacApi.get( + `/rbac/host-access/${hostId}/auth/${protocol}`, + ); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch host authentication override"); + } +} + +export async function setHostAuthOverride( + hostId: number, + protocol: AuthOverrideProtocol, + credentialId: number | null, +): Promise<{ + success: boolean; + protocol: AuthOverrideProtocol; + credentialId: number | null; +}> { + try { + const response = await rbacApi.put( + `/rbac/host-access/${hostId}/auth/${protocol}`, + { credentialId }, + ); + return response.data; + } catch (error) { + throw handleApiError(error, "update host authentication override"); + } +} + // ============================================================================ // SNIPPET SHARING // ============================================================================ diff --git a/src/ui/api/session-sharing-api.ts b/src/ui/api/session-sharing-api.ts new file mode 100644 index 0000000..6b8ca0a --- /dev/null +++ b/src/ui/api/session-sharing-api.ts @@ -0,0 +1,181 @@ +import axios from "axios"; +import { getBasePath } from "@/lib/base-path"; +import { isElectron } from "@/lib/electron"; +import { authApi, handleApiError } from "@/main-axios"; + +export interface ResolvedShareLink { + protocol: "ssh" | "rdp" | "vnc" | "telnet"; + permissionLevel: "read-only" | "read-write"; + wsPath: string; + connectParams?: { token: string }; +} + +export type ShareLinkErrorKind = "not-found" | "rate-limited" | "unknown"; + +export class ShareLinkError extends Error { + constructor( + message: string, + public readonly kind: ShareLinkErrorKind, + ) { + super(message); + this.name = "ShareLinkError"; + } +} + +const isDev = (): boolean => + !isElectron() && + process.env.NODE_ENV === "development" && + (window.location.port === "3000" || + window.location.port === "5173" || + window.location.port === ""); + +// Guests have no session/JWT, so this deliberately builds a bare base URL +// rather than going through main-axios's authenticated instances. The +// desktop app always runs its embedded local backend as the source of +// truth, so a share link opened there always resolves against it -- +// joining a session hosted on someone else's remote server isn't +// supported from the desktop app today. +async function resolveApiBaseUrl(): Promise { + if (isDev()) { + const protocol = window.location.protocol === "https:" ? "https" : "http"; + return `${protocol}://localhost:30001`; + } + if (isElectron()) { + return "http://127.0.0.1:30001"; + } + return getBasePath(); +} + +export async function resolveShareLink( + linkToken: string, +): Promise { + const baseUrl = await resolveApiBaseUrl(); + try { + const response = await axios.get( + `${baseUrl}/session-sharing/resolve/${encodeURIComponent(linkToken)}`, + ); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response?.status === 404) { + throw new ShareLinkError( + "Share link is invalid, expired, or revoked", + "not-found", + ); + } + if (error.response?.status === 429) { + throw new ShareLinkError( + "Too many attempts, please try again shortly", + "rate-limited", + ); + } + } + throw new ShareLinkError("Failed to resolve share link", "unknown"); + } +} + +// ============================================================================ +// SESSION SHARING (authenticated owner-side API) +// ============================================================================ + +export type SessionShareProtocol = "ssh" | "rdp" | "vnc" | "telnet"; +export type SessionShareType = "link" | "user"; +export type SessionSharePermissionLevel = "read-only" | "read-write"; + +export interface SessionShareRecord { + id: string; + hostId: number; + ownerUserId: string; + protocol: SessionShareProtocol; + sessionId: string; + tabInstanceId: string | null; + shareType: SessionShareType; + targetUserId: string | null; + linkToken: string | null; + permissionLevel: SessionSharePermissionLevel; + createdAt: string; + expiresAt: string; + revokedAt: string | null; + lastJoinedAt: string | null; + joinCount: number; +} + +export interface CreateSessionShareRequest { + hostId: number; + sessionId: string; + tabInstanceId?: string; + protocol: SessionShareProtocol; + shareType: SessionShareType; + targetUserId?: string; + permissionLevel: SessionSharePermissionLevel; + expiryHours?: number; +} + +export interface CreateSessionShareResponse { + shareId: string; + linkToken: string | null; + expiresAt: string; +} + +export async function createSessionShare( + request: CreateSessionShareRequest, +): Promise { + try { + const response = await authApi.post("/session-sharing/create", request); + return response.data; + } catch (error) { + throw handleApiError(error, "create session share"); + } +} + +export async function getActiveSessionShares( + hostId: number, +): Promise<{ shares: SessionShareRecord[] }> { + try { + const response = await authApi.get( + `/session-sharing/host/${hostId}/active`, + ); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch active session shares"); + } +} + +export async function revokeSessionShare( + shareId: string, +): Promise<{ success: true }> { + try { + const response = await authApi.delete(`/session-sharing/${shareId}`); + return response.data; + } catch (error) { + throw handleApiError(error, "revoke session share"); + } +} + +// ============================================================================ +// GLOBAL ADMIN TOGGLE +// ============================================================================ + +export async function getSessionSharingGloballyEnabled(): Promise<{ + enabled: boolean; +}> { + try { + const response = await authApi.get("/users/session-sharing-enabled"); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch session sharing enabled setting"); + } +} + +export async function updateSessionSharingGloballyEnabled( + enabled: boolean, +): Promise<{ enabled: boolean }> { + try { + const response = await authApi.patch("/users/session-sharing-enabled", { + enabled, + }); + return response.data; + } catch (error) { + throw handleApiError(error, "update session sharing enabled setting"); + } +} diff --git a/src/ui/api/settings-api.ts b/src/ui/api/settings-api.ts index 4eeed20..96c0540 100644 --- a/src/ui/api/settings-api.ts +++ b/src/ui/api/settings-api.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import { authApi, handleApiError, statsApi } from "@/main-axios"; // GLOBAL MONITORING SETTINGS @@ -77,6 +78,7 @@ export async function updateSessionTimeout( export async function getTailscaleSettings(): Promise<{ apiKey: string; hasApiKey: boolean; + apiBaseUrl: string; }> { try { const response = await authApi.get("/users/tailscale-settings"); @@ -88,10 +90,12 @@ export async function getTailscaleSettings(): Promise<{ export async function updateTailscaleSettings( apiKey: string, + apiBaseUrl?: string, ): Promise<{ hasApiKey: boolean }> { try { const response = await authApi.patch("/users/tailscale-settings", { apiKey, + apiBaseUrl, }); return response.data; } catch (error) { @@ -109,12 +113,29 @@ export async function getTailscaleDevices(): Promise<{ lastSeen: string; }>; hasApiKey: boolean; + error?: string; }> { try { const response = await authApi.get("/tailscale/devices"); return response.data; } catch (error) { + if (axios.isAxiosError(error)) { + const data = error.response?.data; + if ( + data && + typeof data === "object" && + "hasApiKey" in data && + typeof data.hasApiKey === "boolean" + ) { + return data as { + devices: []; + hasApiKey: boolean; + error?: string; + }; + } + } handleApiError(error, "fetch Tailscale devices"); + throw error; } } @@ -145,6 +166,108 @@ export async function updateGuacamoleSettings(settings: { } } +// ============================================================================ +// ANALYTICS SETTINGS +// ============================================================================ + +export async function getAnalyticsEnabled(): Promise<{ + enabled: boolean; + locked?: boolean; +}> { + try { + const response = await authApi.get("/users/analytics-enabled"); + return response.data; + } catch (error) { + handleApiError(error, "fetch analytics enabled setting"); + } +} + +export async function updateAnalyticsEnabled( + enabled: boolean, +): Promise<{ enabled: boolean }> { + try { + const response = await authApi.patch("/users/analytics-enabled", { + enabled, + }); + return response.data; + } catch (error) { + handleApiError(error, "update analytics enabled setting"); + } +} + +// ============================================================================ +// TERMINAL IMAGE STORAGE SETTINGS +// ============================================================================ + +export type TerminalImageStorageMode = "auto" | "local" | "remote-sftp"; + +/** Public settings shape: the backend-internal localDir is never returned. */ +export interface TerminalImageStorageSettings { + mode: TerminalImageStorageMode; + hostPath: string; + ttlMs: number; + maxCount: number; + maxBytes: number; + localMappingConfigured: boolean; +} + +export interface TerminalImageStorageSettingsUpdate { + mode?: TerminalImageStorageMode; + localDir?: string; + hostPath?: string; + ttlMs?: number; + maxCount?: number; + maxBytes?: number; +} + +export interface TerminalImageStorageTestResult { + mode: TerminalImageStorageMode; + connected: boolean; + remoteSftpAvailable: boolean; + localHostVisible: boolean | null; + selectedMode: "local" | "remote-sftp" | "unavailable"; + localMappingConfigured: boolean; +} + +export async function getTerminalImageStorageSettings(): Promise { + try { + const response = await authApi.get( + "/users/terminal-image-storage-settings", + ); + return response.data; + } catch (error) { + handleApiError(error, "fetch terminal image storage settings"); + } +} + +export async function updateTerminalImageStorageSettings( + settings: TerminalImageStorageSettingsUpdate, +): Promise { + try { + const response = await authApi.patch( + "/users/terminal-image-storage-settings", + settings, + ); + return response.data; + } catch (error) { + handleApiError(error, "update terminal image storage settings"); + } +} + +export async function testTerminalImageStorage( + instanceId: string, +): Promise { + try { + const response = await authApi.post( + "/users/terminal-image-storage-settings/test", + { instanceId }, + ); + return response.data; + } catch (error) { + handleApiError(error, "test terminal image storage"); + } +} + // ============================================================================ // HOST DEFAULTS SETTINGS // ============================================================================ diff --git a/src/ui/api/snippets-api.ts b/src/ui/api/snippets-api.ts index 92ee345..fb63e25 100644 --- a/src/ui/api/snippets-api.ts +++ b/src/ui/api/snippets-api.ts @@ -9,6 +9,9 @@ export interface NetworkTopologyNode { tags?: string[]; parent?: string; color?: string; + /** Absent on nodes; callers tell nodes from edges by testing these. */ + source?: undefined; + target?: undefined; }; position?: { x: number; y: number }; } @@ -18,6 +21,8 @@ export interface NetworkTopologyEdge { id?: string; source: string; target: string; + label?: undefined; + ip?: undefined; }; } @@ -26,7 +31,16 @@ export interface NetworkTopologyData { edges: NetworkTopologyEdge[]; } -export async function getSnippets(): Promise> { +/** + * A snippet row as the list endpoint returns it. Callers that need the full + * shape narrow it themselves; only the id is relied on across the app. + */ +export interface SnippetRow { + id: number; + [key: string]: unknown; +} + +export async function getSnippets(): Promise { try { const response = await authApi.get("/snippets"); return response.data; @@ -72,11 +86,13 @@ export async function deleteSnippet( export async function executeSnippet( snippetId: number, hostId: number, + inputValues?: Record, ): Promise<{ success: boolean; output: string; error?: string }> { try { const response = await authApi.post("/snippets/execute", { snippetId, hostId, + ...(inputValues ? { inputValues } : {}), }); return response.data; } catch (error) { diff --git a/src/ui/api/sse-stream.ts b/src/ui/api/sse-stream.ts new file mode 100644 index 0000000..d438aa2 --- /dev/null +++ b/src/ui/api/sse-stream.ts @@ -0,0 +1,46 @@ +export interface ServerSentEvent { + event: string; + data: string; +} + +export async function streamServerSentEvents( + url: string, + init: RequestInit, + onEvent: (event: ServerSentEvent) => void, +): Promise { + const response = await fetch(url, init); + if (!response.ok) { + throw new Error(`SSE request failed with status ${response.status}`); + } + if (!response.body) throw new Error("SSE response has no body"); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let eventName = "message"; + let data: string[] = []; + + const dispatch = () => { + if (data.length > 0) onEvent({ event: eventName, data: data.join("\n") }); + eventName = "message"; + data = []; + }; + + while (true) { + const { done, value } = await reader.read(); + buffer += decoder.decode(value, { stream: !done }); + const lines = buffer.split(/\r?\n/); + buffer = done ? "" : (lines.pop() ?? ""); + + for (const line of lines) { + if (line === "") dispatch(); + else if (line.startsWith("event:")) eventName = line.slice(6).trimStart(); + else if (line.startsWith("data:")) data.push(line.slice(5).trimStart()); + } + + if (done) { + dispatch(); + return; + } + } +} diff --git a/src/ui/api/ssh-file-operations-api.ts b/src/ui/api/ssh-file-operations-api.ts index 082f4c9..3984829 100644 --- a/src/ui/api/ssh-file-operations-api.ts +++ b/src/ui/api/ssh-file-operations-api.ts @@ -1,7 +1,23 @@ +import { getErrorMessage } from "../lib/error-message.js"; import axios from "axios"; -import { authApi, fileManagerApi, handleApiError } from "@/main-axios"; +import { asHttpError } from "@/lib/http-error"; +import { + authApi, + fileManagerApi, + handleApiError, + getFileManagerApiForSession, + setSessionOrigin, + clearSessionOrigin, +} from "@/main-axios"; +import { resolveConnectionOrigin } from "@/lib/connection-origin"; import { fileLogger } from "@/lib/frontend-logger"; -import type { SSHHost } from "@/types/index"; +import type { FileItem, SSHHost } from "@/types/index"; +import { getCachedFileList } from "@/lib/file-list-request-cache"; +import { + getCachedFileContent, + invalidateCachedFileContent, + type FileContentResult, +} from "@/lib/file-content-request-cache"; type ApiConnectionLog = { type: "info" | "success" | "warning" | "error"; @@ -24,6 +40,22 @@ type ConnectErrorResponse = { reason?: string; }; +/** + * The interactive-auth branches /ssh/connect can take before a session exists. + * Callers switch on these, so they cannot stay behind Record. + */ +export interface SSHConnectResult { + success?: boolean; + status?: string; + reason?: "timeout" | "no_keyboard" | "auth_failed"; + requires_totp?: boolean; + requires_warpgate?: boolean; + sessionId?: string; + prompt?: string; + url?: string; + securityKey?: string; +} + function buildFileManagerUrl(path: string): string { const baseURL = String(fileManagerApi.defaults.baseURL || ""); return `${baseURL.replace(/\/$/, "")}${path}`; @@ -52,6 +84,8 @@ export async function connectSSH( sessionId: string, config: { hostId?: number; + /** Names the host across a sync pair; hostId only names it locally. */ + syncId?: string | null; ip: string; port: number; username: string; @@ -70,9 +104,9 @@ export async function connectSSH( socks5ProxyChain?: unknown; jumpHosts?: Array<{ hostId: number }>; }, -): Promise> { +): Promise { try { - const response = await fileManagerApi.post( + const response = await getFileManagerApiForSession(sessionId).post( "/ssh/connect", { sessionId, ...config }, { timeout: 120000 }, @@ -121,12 +155,15 @@ export async function disconnectSSH( sessionId: string, ): Promise> { try { - const response = await fileManagerApi.post("/ssh/disconnect", { - sessionId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/disconnect", + { sessionId }, + ); return response.data; } catch (error) { handleApiError(error, "disconnect SSH"); + } finally { + clearSessionOrigin(sessionId); } } @@ -135,10 +172,10 @@ export async function verifySSHTOTP( totpCode: string, ): Promise> { try { - const response = await fileManagerApi.post("/ssh/connect-totp", { - sessionId, - totpCode, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/connect-totp", + { sessionId, totpCode }, + ); return response.data; } catch (error) { handleApiError(error, "verify SSH TOTP"); @@ -149,9 +186,10 @@ export async function verifySSHWarpgate( sessionId: string, ): Promise> { try { - const response = await fileManagerApi.post("/ssh/connect-warpgate", { - sessionId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/connect-warpgate", + { sessionId }, + ); return response.data; } catch (error) { handleApiError(error, "verify SSH Warpgate"); @@ -239,9 +277,10 @@ export async function getSSHStatus( sessionId: string, ): Promise<{ connected: boolean }> { try { - const response = await fileManagerApi.get("/ssh/status", { - params: { sessionId }, - }); + const response = await getFileManagerApiForSession(sessionId).get( + "/ssh/status", + { params: { sessionId } }, + ); return response.data; } catch (error) { handleApiError(error, "get SSH status"); @@ -252,9 +291,10 @@ export async function keepSSHAlive( sessionId: string, ): Promise> { try { - const response = await fileManagerApi.post("/ssh/keepalive", { - sessionId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/keepalive", + { sessionId }, + ); return response.data; } catch (error) { handleApiError(error, "SSH keepalive"); @@ -264,16 +304,24 @@ export async function keepSSHAlive( export async function listSSHFiles( sessionId: string, path: string, -): Promise<{ files: unknown[]; path: string }> { - try { - const response = await fileManagerApi.get("/ssh/listFiles", { - params: { sessionId, path }, - }); - return response.data || { files: [], path }; - } catch (error) { - handleApiError(error, "list SSH files"); - return { files: [], path }; - } + options: { force?: boolean } = {}, +): Promise<{ files: FileItem[]; path: string }> { + return getCachedFileList( + sessionId, + path, + async () => { + try { + const response = await getFileManagerApiForSession(sessionId).get( + "/ssh/listFiles", + { params: { sessionId, path } }, + ); + return response.data || { files: [], path }; + } catch (error) { + handleApiError(error, "list SSH files"); + } + }, + options.force, + ); } export async function identifySSHSymlink( @@ -281,9 +329,10 @@ export async function identifySSHSymlink( path: string, ): Promise<{ path: string; target: string; type: "directory" | "file" }> { try { - const response = await fileManagerApi.get("/ssh/identifySymlink", { - params: { sessionId, path }, - }); + const response = await getFileManagerApiForSession(sessionId).get( + "/ssh/identifySymlink", + { params: { sessionId, path } }, + ); return response.data; } catch (error) { handleApiError(error, "identify SSH symlink"); @@ -295,9 +344,10 @@ export async function resolveSSHPath( path: string, ): Promise { try { - const response = await fileManagerApi.get("/ssh/resolvePath", { - params: { sessionId, path }, - }); + const response = await getFileManagerApiForSession(sessionId).get( + "/ssh/resolvePath", + { params: { sessionId, path } }, + ); return response.data?.resolvedPath || path; } catch { return path; @@ -307,29 +357,41 @@ export async function resolveSSHPath( export async function readSSHFile( sessionId: string, path: string, -): Promise<{ - content: string; - path: string; - encoding?: "base64" | "utf8"; -}> { - try { - const response = await fileManagerApi.get("/ssh/readFile", { - params: { sessionId, path }, - }); - return response.data; - } catch (error: unknown) { - if (error.response?.status === 404) { - const customError = new Error("File not found"); - ( - customError as Error & { response?: unknown; isFileNotFound?: boolean } - ).response = error.response; - ( - customError as Error & { response?: unknown; isFileNotFound?: boolean } - ).isFileNotFound = error.response.data?.fileNotFound || true; - throw customError; - } - handleApiError(error, "read SSH file"); - } + options: { force?: boolean } = {}, +): Promise { + return getCachedFileContent( + sessionId, + path, + async () => { + try { + const response = await getFileManagerApiForSession(sessionId).get( + "/ssh/readFile", + { params: { sessionId, path } }, + ); + return response.data; + } catch (error: unknown) { + const httpError = asHttpError(error); + if (httpError.response?.status === 404) { + const customError = new Error("File not found"); + ( + customError as Error & { + response?: unknown; + isFileNotFound?: boolean; + } + ).response = httpError.response; + ( + customError as Error & { + response?: unknown; + isFileNotFound?: boolean; + } + ).isFileNotFound = httpError.response.data?.fileNotFound || true; + throw customError; + } + handleApiError(error, "read SSH file"); + } + }, + options.force, + ); } export async function writeSSHFile( @@ -340,19 +402,17 @@ export async function writeSSHFile( userId?: string, ): Promise> { try { - const response = await fileManagerApi.post("/ssh/writeFile", { - sessionId, - path, - content, - hostId, - userId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/writeFile", + { sessionId, path, content, hostId, userId }, + ); if ( response.data && (response.data.message === "File written successfully" || response.status === 200) ) { + invalidateCachedFileContent(sessionId, path); return response.data; } else { throw new Error("File write operation did not return success status"); @@ -410,7 +470,7 @@ export async function uploadSSHFile( form.append("totalSize", String(file.size)); form.append("chunk", chunkBlob, fileName); - const response = await fileManagerApi.postForm( + const response = await getFileManagerApiForSession(sessionId).postForm( "/ssh/uploadFileChunk", form, { timeout: 0 }, @@ -444,7 +504,7 @@ export async function uploadSSHFile( if (userId !== undefined) form.append("userId", userId); form.append("file", file, fileName); - const response = await fileManagerApi.postForm( + const response = await getFileManagerApiForSession(sessionId).postForm( "/ssh/uploadFileStream", form, { @@ -457,14 +517,23 @@ export async function uploadSSHFile( } } +export interface DownloadedSSHFile { + /** base64-encoded file contents */ + content: string; + fileName: string; + size: number; + mimeType: string; + path: string; +} + export async function downloadSSHFile( sessionId: string, filePath: string, hostId?: number, userId?: string, -): Promise> { +): Promise { try { - const response = await fileManagerApi.post( + const response = await getFileManagerApiForSession(sessionId).post( "/ssh/downloadFile", { sessionId, @@ -484,7 +553,7 @@ export async function downloadSSHFileStream( sessionId: string, filePath: string, ): Promise { - const response = await fileManagerApi.post( + const response = await getFileManagerApiForSession(sessionId).post( "/ssh/downloadFileStream", { sessionId, path: filePath }, { responseType: "blob", timeout: 0 }, @@ -503,14 +572,10 @@ export async function createSSHFile( userId?: string, ): Promise> { try { - const response = await fileManagerApi.post("/ssh/createFile", { - sessionId, - path, - fileName, - content, - hostId, - userId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/createFile", + { sessionId, path, fileName, content, hostId, userId }, + ); return response.data; } catch (error) { handleApiError(error, "create SSH file"); @@ -525,13 +590,10 @@ export async function createSSHFolder( userId?: string, ): Promise> { try { - const response = await fileManagerApi.post("/ssh/createFolder", { - sessionId, - path, - folderName, - hostId, - userId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/createFolder", + { sessionId, path, folderName, hostId, userId }, + ); return response.data; } catch (error) { handleApiError(error, "create SSH folder"); @@ -544,29 +606,115 @@ export async function deleteSSHItem( isDirectory: boolean, hostId?: number, userId?: string, + permanent = false, ): Promise> { try { - const response = await fileManagerApi.delete("/ssh/deleteItem", { - data: { - sessionId, - path, - isDirectory, - hostId, - userId, + const response = await getFileManagerApiForSession(sessionId).delete( + "/ssh/deleteItem", + { + data: { + sessionId, + path, + isDirectory, + hostId, + userId, + permanent, + }, }, - }); + ); + if (!isDirectory) invalidateCachedFileContent(sessionId, path); return response.data; } catch (error) { + if ( + axios.isAxiosError(error) && + error.response?.data?.trashUnavailable === true + ) { + throw error; + } handleApiError(error, "delete SSH item"); } } +export interface TrashItem { + id: string; + name: string; + originalPath: string; + isDirectory: boolean; + deletedAt: string; + size: number; +} + +export async function getSSHTrash(sessionId: string): Promise<{ + items: TrashItem[]; + retentionDays: number; + canManageRetention: boolean; +}> { + try { + const response = await getFileManagerApiForSession(sessionId).get( + "/ssh/trash", + { params: { sessionId } }, + ); + return response.data; + } catch (error) { + handleApiError(error, "list SSH trash"); + } +} + +export async function restoreSSHTrashItem(sessionId: string, id: string) { + try { + const response = await getFileManagerApiForSession(sessionId).post( + `/ssh/trash/${encodeURIComponent(id)}/restore`, + { sessionId }, + ); + return response.data; + } catch (error) { + handleApiError(error, "restore SSH trash item"); + } +} + +export async function permanentlyDeleteSSHTrashItem( + sessionId: string, + id: string, +) { + try { + await getFileManagerApiForSession(sessionId).delete( + `/ssh/trash/${encodeURIComponent(id)}`, + { data: { sessionId } }, + ); + } catch (error) { + handleApiError(error, "permanently delete SSH trash item"); + } +} + +export async function emptySSHTrash(sessionId: string) { + try { + await getFileManagerApiForSession(sessionId).delete("/ssh/trash", { + data: { sessionId }, + }); + } catch (error) { + handleApiError(error, "empty SSH trash"); + } +} + +export async function updateSSHTrashRetention( + sessionId: string, + retentionDays: number, +) { + try { + await getFileManagerApiForSession(sessionId).put("/ssh/trash-retention", { + retentionDays, + }); + } catch (error) { + handleApiError(error, "update SSH trash retention"); + } +} + export async function setSudoPassword( sessionId: string, password: string, ): Promise { try { - await fileManagerApi.post("/sudo-password", { + await getFileManagerApiForSession(sessionId).post("/sudo-password", { sessionId, password, }); @@ -575,15 +723,22 @@ export async function setSudoPassword( } } +export interface CopySSHItemResult { + message?: string; + /** Set when the copy was renamed to avoid clobbering an existing entry. */ + uniqueName?: string; + targetPath?: string; +} + export async function copySSHItem( sessionId: string, sourcePath: string, targetDir: string, hostId?: number, userId?: string, -): Promise> { +): Promise { try { - const response = await fileManagerApi.post( + const response = await getFileManagerApiForSession(sessionId).post( "/ssh/copyItem", { sessionId, @@ -611,13 +766,14 @@ export async function renameSSHItem( userId?: string, ): Promise> { try { - const response = await fileManagerApi.put("/ssh/renameItem", { - sessionId, - oldPath, - newName, - hostId, - userId, - }); + const response = await getFileManagerApiForSession(sessionId).put( + "/ssh/renameItem", + { sessionId, oldPath, newName, hostId, userId }, + ); + invalidateCachedFileContent(sessionId, oldPath); + const separator = oldPath.lastIndexOf("/"); + const newPath = `${oldPath.slice(0, separator + 1)}${newName}`; + invalidateCachedFileContent(sessionId, newPath); return response.data; } catch (error) { handleApiError(error, "rename SSH item"); @@ -633,7 +789,7 @@ export async function moveSSHItem( userId?: string, ): Promise> { try { - const response = await fileManagerApi.put( + const response = await getFileManagerApiForSession(sessionId).put( "/ssh/moveItem", { sessionId, @@ -646,6 +802,8 @@ export async function moveSSHItem( timeout: 60000, }, ); + invalidateCachedFileContent(sessionId, oldPath); + invalidateCachedFileContent(sessionId, newPath); return response.data; } catch (error) { handleApiError(error, "move SSH item"); @@ -670,13 +828,10 @@ export async function changeSSHPermissions( userId, }); - const response = await fileManagerApi.post("/ssh/changePermissions", { - sessionId, - path, - permissions, - hostId, - userId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/changePermissions", + { sessionId, path, permissions, hostId, userId }, + ); fileLogger.success("SSH file permissions changed successfully", { operation: "change_permissions", @@ -715,13 +870,10 @@ export async function extractSSHArchive( userId, }); - const response = await fileManagerApi.post("/ssh/extractArchive", { - sessionId, - archivePath, - extractPath, - hostId, - userId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/extractArchive", + { sessionId, archivePath, extractPath, hostId, userId }, + ); fileLogger.success("Archive extracted successfully", { operation: "extract_archive", @@ -762,14 +914,17 @@ export async function compressSSHFiles( userId, }); - const response = await fileManagerApi.post("/ssh/compressFiles", { - sessionId, - paths, - archiveName, - format: format || "zip", - hostId, - userId, - }); + const response = await getFileManagerApiForSession(sessionId).post( + "/ssh/compressFiles", + { + sessionId, + paths, + archiveName, + format: format || "zip", + hostId, + userId, + }, + ); fileLogger.success("Files compressed successfully", { operation: "compress_files", @@ -795,11 +950,7 @@ export async function compressSSHFiles( // ============================================================================ export type HostConnectionState = - | "disconnected" - | "connecting" - | "ready" - | "auth_required" - | "error"; + "disconnected" | "connecting" | "ready" | "auth_required" | "error"; export interface EnsureSSHSessionResult { state: HostConnectionState; @@ -811,6 +962,12 @@ export async function ensureSSHSessionForHost( host: SSHHost, ): Promise { const sessionId = host.id.toString(); + const origin = await resolveConnectionOrigin({ + connectionType: host.connectionType, + connectionOrigin: host.connectionOrigin, + }); + setSessionOrigin(sessionId, origin); + try { const status = await getSSHStatus(sessionId); if (status?.connected) { @@ -823,6 +980,7 @@ export async function ensureSSHSessionForHost( try { const result = await connectSSH(sessionId, { hostId: host.id, + syncId: host.syncId ?? null, ip: host.ip, port: host.port, username: host.username, @@ -852,7 +1010,7 @@ export async function ensureSSHSessionForHost( return { state: "ready", sessionId }; } catch (err) { - const message = err instanceof Error ? err.message : "Connection failed"; + const message = getErrorMessage(err, "Connection failed"); return { state: "error", error: message }; } } diff --git a/src/ui/api/ssh-host-management-api.ts b/src/ui/api/ssh-host-management-api.ts index 294a65d..9f6f2cf 100644 --- a/src/ui/api/ssh-host-management-api.ts +++ b/src/ui/api/ssh-host-management-api.ts @@ -12,6 +12,11 @@ import { getCachedSSHHosts, invalidateHostsAndStatusCaches, } from "@/lib/hosts-request-cache"; +import { requestRemoteSync } from "@/lib/remote-sync-trigger"; +import { + getConnectedRemoteApi, + markRemoteSharedHosts, +} from "@/lib/remote-server-api"; // SSH HOST MANAGEMENT // ============================================================================ @@ -23,7 +28,22 @@ export type GetSSHHostsOptions = { async function loadSSHHostsFromApi(): Promise { const hostsResponse = await sshHostApi.get("/db/host"); - return Array.isArray(hostsResponse.data) ? hostsResponse.data : []; + const localHosts = Array.isArray(hostsResponse.data) + ? hostsResponse.data + : []; + const remoteApi = await getConnectedRemoteApi(); + if (!remoteApi) return localHosts; + + try { + const remoteResponse = await remoteApi.get("/host/db/host"); + const remoteSharedHosts = Array.isArray(remoteResponse.data) + ? markRemoteSharedHosts(remoteResponse.data) + : []; + return [...localHosts, ...remoteSharedHosts]; + } catch { + // Keep the last locally synced host set usable while the server is offline. + return localHosts; + } } export async function getSSHHosts( @@ -68,10 +88,12 @@ export async function createSSHHost(hostData: SSHHostData): Promise { headers: { "Content-Type": "multipart/form-data" }, }); invalidateHostsAndStatusCaches(); + void requestRemoteSync(); return response.data; } const response = await sshHostApi.post("/db/host", hostData); invalidateHostsAndStatusCaches(); + void requestRemoteSync(); return response.data; } catch (error) { throw handleApiError(error, "create SSH host"); @@ -92,10 +114,12 @@ export async function updateSSHHost( headers: { "Content-Type": "multipart/form-data" }, }); invalidateHostsAndStatusCaches(); + void requestRemoteSync(); return response.data; } const response = await sshHostApi.put(`/db/host/${hostId}`, hostData); invalidateHostsAndStatusCaches(); + void requestRemoteSync(); return response.data; } catch (error) { throw handleApiError(error, "update SSH host"); @@ -174,6 +198,60 @@ export async function discoverProxmoxGuests( } } +export function discoverProxmoxGuestsStream( + hostId: number, + handlers: { + onProgress?: (done: number, total: number) => void; + onResult: (result: ProxmoxDiscoverResult) => void; + onError: (message: string) => void; + }, +): () => void { + const baseURL = (authApi.defaults.baseURL || "").replace(/\/$/, ""); + const source = new EventSource( + `${baseURL}/proxmox/discover/stream?hostId=${encodeURIComponent( + String(hostId), + )}`, + { withCredentials: true }, + ); + let settled = false; + const close = () => { + settled = true; + source.close(); + }; + source.addEventListener("progress", (event) => { + try { + const data = JSON.parse((event as MessageEvent).data); + handlers.onProgress?.(data.done, data.total); + } catch { + // ignore malformed progress frames + } + }); + source.addEventListener("result", (event) => { + close(); + try { + handlers.onResult(JSON.parse((event as MessageEvent).data)); + } catch { + handlers.onError("Failed to parse discovery result"); + } + }); + source.addEventListener("fail", (event) => { + close(); + let message = "Discovery failed"; + try { + message = JSON.parse((event as MessageEvent).data).message || message; + } catch { + // keep default message + } + handlers.onError(message); + }); + source.onerror = () => { + if (settled) return; + close(); + handlers.onError("Discovery connection lost"); + }; + return close; +} + export async function syncProxmoxGuests( hostId: number, ): Promise { @@ -205,12 +283,25 @@ export async function bulkUpdateSSHHosts( } } +export async function reorderSSHHosts( + positions: { id: number; sortOrder: number }[], +): Promise<{ updated: number }> { + try { + const response = await sshHostApi.put("/reorder", { positions }); + invalidateHostsAndStatusCaches(); + return response.data; + } catch (error) { + handleApiError(error, "reorder SSH hosts"); + } +} + export async function deleteSSHHost( hostId: number, ): Promise> { try { const response = await sshHostApi.delete(`/db/host/${hostId}`); invalidateHostsAndStatusCaches(); + void requestRemoteSync(); return response.data; } catch (error) { handleApiError(error, "delete SSH host"); diff --git a/src/ui/api/system-status-api.ts b/src/ui/api/system-status-api.ts index 63e42b7..6617b55 100644 --- a/src/ui/api/system-status-api.ts +++ b/src/ui/api/system-status-api.ts @@ -1,11 +1,11 @@ import { AxiosError } from "axios"; +import type { TermixAlert } from "@/types"; import { authApi, handleApiError, - isElectron, markUserAuthenticated, + type AuthResponse, } from "@/main-axios"; -import type { AuthResponse } from "@/main-axios"; // ALERTS // ============================================================================ @@ -63,25 +63,6 @@ export async function verifyTOTPLogin( rememberMe, }); - const isInIframe = - typeof window !== "undefined" && window.self !== window.top; - - if (isInIframe && isElectron() && response.data.success) { - try { - window.parent.postMessage( - { - type: "AUTH_SUCCESS", - source: "totp_verify", - platform: "desktop", - timestamp: Date.now(), - }, - window.location.origin, - ); - } catch (e) { - console.error("[main-axios] Error posting message to parent:", e); - } - } - if (response.data.success) { markUserAuthenticated(); } @@ -110,7 +91,7 @@ export async function generateBackupCodes( } export async function getUserAlerts(): Promise<{ - alerts: Array>; + alerts: TermixAlert[]; }> { try { const response = await authApi.get(`/alerts`); @@ -137,9 +118,34 @@ export async function dismissAlert( // UPDATES & RELEASES // ============================================================================ +export interface ReleaseItem { + id: number; + title: string; + description: string; + link: string; + pubDate: string; + version: string; + isPrerelease: boolean; + isDraft: boolean; + assets: Array<{ + name: string; + size: number; + download_count: number; + download_url: string; + }>; +} + +export interface ReleasesRSSResponse { + feed: { title: string; description: string; link: string; updated: string }; + items: ReleaseItem[]; + total_count: number; + cached: boolean; + cache_age?: number; +} + export async function getReleasesRSS( perPage: number = 100, -): Promise> { +): Promise { try { const response = await authApi.get(`/releases/rss?per_page=${perPage}`); return response.data; @@ -148,9 +154,37 @@ export async function getReleasesRSS( } } -export async function getVersionInfo( - checkRemote = true, -): Promise> { +export interface VersionInfo { + status?: "up_to_date" | "requires_update" | "beta"; + /** Same value as remoteVersion; the endpoint sends both. */ + version?: string; + localVersion?: string; + remoteVersion?: string; + latest_release?: { + tag_name?: string; + name?: string; + published_at?: string; + html_url?: string; + body?: string; + }; + cached?: boolean; + cache_age?: number; + // Callers reach for fields beyond the ones above -- SystemOverviewWidget + // reads `updateAvailable`, which this endpoint does not in fact return -- + // so keep the index signature the previous `Record` gave + // them. Typing those reads out of existence is a separate change. + [key: string]: unknown; +} + +// Where the release page lives inside a version response. Both surfaces that +// render a version badge read it, and an empty string is what they treat as +// "no link to offer", so keep the shape in one place rather than repeating the +// optional chain at each call site. +export function releaseUrlFrom(info: VersionInfo | null | undefined): string { + return info?.latest_release?.html_url ?? ""; +} + +export async function getVersionInfo(checkRemote = true): Promise { try { const response = await authApi.get( `/version${checkRemote ? "" : "?checkRemote=false"}`, diff --git a/src/ui/api/touch-input-settings-api.ts b/src/ui/api/touch-input-settings-api.ts new file mode 100644 index 0000000..a34b435 --- /dev/null +++ b/src/ui/api/touch-input-settings-api.ts @@ -0,0 +1,28 @@ +import { authApi, handleApiError } from "@/main-axios"; +import { + normalizeTouchInputSettings, + type TouchInputSettings, +} from "@/types/touch-input-settings"; + +export async function getTouchInputSettings(): Promise { + try { + const response = await authApi.get("/users/touch-input-settings"); + return normalizeTouchInputSettings(response.data); + } catch (error) { + handleApiError(error, "fetch touch input settings"); + } +} + +export async function updateTouchInputSettings( + settings: TouchInputSettings, +): Promise { + try { + const response = await authApi.patch( + "/users/touch-input-settings", + settings, + ); + return normalizeTouchInputSettings(response.data); + } catch (error) { + handleApiError(error, "update touch input settings"); + } +} diff --git a/src/ui/api/tunnel-api.ts b/src/ui/api/tunnel-api.ts index 311dc73..702254a 100644 --- a/src/ui/api/tunnel-api.ts +++ b/src/ui/api/tunnel-api.ts @@ -1,21 +1,62 @@ import axios from "axios"; -import { authApi, handleApiError, tunnelApi } from "@/main-axios"; +import { + authApi, + handleApiError, + tunnelApi, + getRemoteTunnelApi, + isElectron, +} from "@/main-axios"; import type { C2STunnelPreset, TunnelConfig, TunnelConnection, TunnelStatus, } from "@/types/index"; +import { streamServerSentEvents } from "./sse-stream"; +import { runAdaptivePolling } from "@/lib/adaptive-polling"; // TUNNEL MANAGEMENT // ============================================================================ +// +// Tunnel status is a process-local, in-memory view (no DB lookup) so it's +// safe to read from both the embedded backend and a connected remote server +// and merge the results. connectTunnel/disconnectTunnel/cancelTunnel are +// NOT origin-routed: they resolve the target host by numeric database id +// against whichever backend receives the request, and a synced host has a +// different numeric id in each database (only its syncId matches across +// them) -- routing those calls to a remote backend would need a +// local-id-to-remote-id resolution step that doesn't exist yet. They always +// target the embedded local backend for now. + +async function isRemoteSyncConnected(): Promise { + if (!isElectron()) return false; + try { + const config = (await window.electronAPI?.invoke?.( + "get-remote-sync-config", + )) as { serverUrl?: string } | null; + return !!config?.serverUrl; + } catch { + return false; + } +} export async function getTunnelStatuses(): Promise< Record > { try { - const response = await tunnelApi.get("/tunnel/status"); - return response.data || {}; + const [localResult, remoteConnected] = await Promise.all([ + tunnelApi.get("/tunnel/status"), + isRemoteSyncConnected(), + ]); + const localStatuses = localResult.data || {}; + if (!remoteConnected) return localStatuses; + + try { + const remoteResult = await getRemoteTunnelApi().get("/tunnel/status"); + return { ...localStatuses, ...(remoteResult.data || {}) }; + } catch { + return localStatuses; + } } catch (error) { handleApiError(error, "fetch tunnel statuses"); } @@ -26,23 +67,94 @@ export function subscribeTunnelStatuses( onError?: () => void, ): () => void { const baseURL = (tunnelApi.defaults.baseURL || "").replace(/\/$/, ""); - const source = new EventSource(`${baseURL}/tunnel/status/stream`, { - withCredentials: true, - }); + const controller = new AbortController(); - source.addEventListener("statuses", (event) => { - try { - onStatuses(JSON.parse(event.data) as Record); - } catch { - onError?.(); - } - }); + let latestLocal: Record = {}; + let latestRemote: Record = {}; + let stopRemotePolling: (() => void) | null = null; - source.onerror = () => { - onError?.(); + const emitMerged = () => { + onStatuses({ ...latestLocal, ...latestRemote }); }; - return () => source.close(); + const waitToReconnect = () => + new Promise((resolve) => { + const onAbort = () => { + clearTimeout(timeout); + resolve(); + }; + const timeout = setTimeout(() => { + controller.signal.removeEventListener("abort", onAbort); + resolve(); + }, 1000); + controller.signal.addEventListener("abort", onAbort, { once: true }); + }); + + void (async () => { + while (!controller.signal.aborted) { + const headers = new Headers({ Accept: "text/event-stream" }); + if (isElectron()) { + headers.set("X-Electron-App", "true"); + const jwt = localStorage.getItem("jwt"); + if (jwt) headers.set("Authorization", `Bearer ${jwt}`); + } + try { + await streamServerSentEvents( + `${baseURL}/tunnel/status/stream`, + { credentials: "include", headers, signal: controller.signal }, + (event) => { + if (event.event !== "statuses") return; + try { + latestLocal = JSON.parse(event.data) as Record< + string, + TunnelStatus + >; + emitMerged(); + } catch { + onError?.(); + } + }, + ); + if (!controller.signal.aborted) onError?.(); + } catch (error) { + const aborted = + controller.signal.aborted || + (error instanceof DOMException && error.name === "AbortError"); + if (!aborted) onError?.(); + } + if (!controller.signal.aborted) await waitToReconnect(); + } + })(); + + // Remote tunnel status has no SSE stream exposed to the desktop app yet, + // so poll it at a modest interval when a remote server is connected. + isRemoteSyncConnected().then((connected) => { + if (!connected || controller.signal.aborted) return; + let signature = ""; + stopRemotePolling = runAdaptivePolling( + async () => { + const result = await getRemoteTunnelApi().get("/tunnel/status"); + const next = result.data || {}; + const nextSignature = JSON.stringify(next); + const changed = nextSignature !== signature; + signature = nextSignature; + latestRemote = next; + emitMerged(); + return changed; + }, + { + minIntervalMs: 5000, + maxIntervalMs: 30000, + stablePollsPerStep: 3, + }, + { enabled: () => !controller.signal.aborted }, + ); + }); + + return () => { + controller.abort(); + stopRemotePolling?.(); + }; } export async function getTunnelStatusByName( diff --git a/src/ui/api/ui-preferences-api.ts b/src/ui/api/ui-preferences-api.ts new file mode 100644 index 0000000..94af00f --- /dev/null +++ b/src/ui/api/ui-preferences-api.ts @@ -0,0 +1,34 @@ +import { authApi, handleApiError } from "@/main-axios"; +import { + sanitizeUiPreferences, + type UiPreferences, +} from "@/types/ui-preferences"; + +// UI PREFERENCES API +// ============================================================================ + +export async function getUiPreferences(): Promise { + try { + const response = await authApi.get("/ui-preferences"); + return sanitizeUiPreferences(response.data?.preferences); + } catch (error) { + handleApiError(error, "fetch UI preferences"); + throw error; + } +} + +/** + * Sends a partial document. The backend merges overrides two levels deep, so + * only changed keys need to be sent; a null clears an override and hands the + * knob back to the preset. + */ +export async function saveUiPreferences( + preferences: Partial & Record, +): Promise { + try { + await authApi.put("/ui-preferences", preferences); + } catch (error) { + handleApiError(error, "save UI preferences"); + throw error; + } +} diff --git a/src/ui/api/user-management-api.ts b/src/ui/api/user-management-api.ts index a3ebdb9..25e6568 100644 --- a/src/ui/api/user-management-api.ts +++ b/src/ui/api/user-management-api.ts @@ -1,12 +1,29 @@ -import { authApi, handleApiError } from "@/main-axios"; -import type { UserInfo } from "@/main-axios"; +import { authApi, handleApiError, type UserInfo } from "@/main-axios"; +import { getConnectedRemoteApi } from "@/lib/remote-server-api"; // USER MANAGEMENT // ============================================================================ -export async function getUserList(): Promise<{ users: UserInfo[] }> { +export type UserListOptions = { + /** Case-insensitive username substring filter. */ + search?: string; + /** Page size. Omit to fetch every user (what the share pickers want). */ + limit?: number; + offset?: number; +}; + +export async function getUserList( + options: UserListOptions = {}, +): Promise<{ users: UserInfo[]; total?: number }> { try { - const response = await authApi.get("/users/list"); + const api = (await getConnectedRemoteApi()) ?? authApi; + const response = await api.get("/users/list", { + params: { + ...(options.search ? { search: options.search } : {}), + ...(options.limit ? { limit: options.limit } : {}), + ...(options.offset ? { offset: options.offset } : {}), + }, + }); return response.data; } catch (error) { handleApiError(error, "fetch user list"); diff --git a/src/ui/api/webauthn-api.ts b/src/ui/api/webauthn-api.ts index 76f2f5b..5c5d0ae 100644 --- a/src/ui/api/webauthn-api.ts +++ b/src/ui/api/webauthn-api.ts @@ -1,14 +1,9 @@ import type { - AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, } from "@simplewebauthn/browser"; -import { - startAuthentication, - startRegistration, -} from "@simplewebauthn/browser"; -import { authApi, handleApiError, type AuthResponse } from "@/main-axios"; +import { startRegistration } from "@simplewebauthn/browser"; +import { authApi, handleApiError } from "@/main-axios"; export type WebAuthnUserVerification = "discouraged" | "preferred" | "required"; @@ -28,11 +23,6 @@ type RegistrationOptionsResponse = { challengeId: string; }; -type AuthenticationOptionsResponse = { - options: PublicKeyCredentialRequestOptionsJSON; - challengeId: string; -}; - export async function listWebAuthnCredentials(): Promise<{ credentials: WebAuthnCredentialSummary[]; }> { @@ -70,41 +60,6 @@ export async function registerWebAuthnCredential( } } -export async function authenticateWithWebAuthn( - username: string, - rememberMe: boolean, - userVerification: WebAuthnUserVerification = "preferred", -): Promise { - try { - const optionsResponse = await authApi.post( - "/users/webauthn/authenticate/options", - { - username: username.trim() || undefined, - userVerification, - }, - ); - const credential = await startAuthentication({ - optionsJSON: optionsResponse.data.options, - }); - const verifyResponse = await authApi.post( - "/users/webauthn/authenticate/verify", - { - challengeId: optionsResponse.data.challengeId, - rememberMe, - response: credential as AuthenticationResponseJSON, - }, - ); - - if (verifyResponse.data.token) { - localStorage.setItem("jwt", verifyResponse.data.token); - } - - return verifyResponse.data; - } catch (error) { - throw handleApiError(error, "authenticate with passkey"); - } -} - export async function deleteWebAuthnCredential( credentialId: string, ): Promise<{ success: boolean }> { diff --git a/src/ui/api/workspaces-api.ts b/src/ui/api/workspaces-api.ts new file mode 100644 index 0000000..01f4c4a --- /dev/null +++ b/src/ui/api/workspaces-api.ts @@ -0,0 +1,125 @@ +import { authApi, handleApiError } from "@/main-axios"; +import type { Workspace, WorkspacePayload } from "@/types/ui-types"; + +export async function listWorkspaces(): Promise { + try { + const response = await authApi.get("/workspaces"); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch workspaces"); + } +} + +export async function createWorkspace(data: { + name: string; + color?: string | null; + icon?: string | null; + payload: WorkspacePayload; +}): Promise { + try { + const response = await authApi.post("/workspaces", data); + return response.data; + } catch (error) { + throw handleApiError(error, "create workspace"); + } +} + +export async function renameWorkspace( + id: number, + data: { name?: string; color?: string | null; icon?: string | null }, +): Promise { + try { + const response = await authApi.patch(`/workspaces/${id}`, data); + return response.data; + } catch (error) { + throw handleApiError(error, "update workspace"); + } +} + +export async function updateWorkspaceContent( + id: number, + payload: WorkspacePayload, +): Promise { + try { + const response = await authApi.put(`/workspaces/${id}/content`, { + payload, + }); + return response.data; + } catch (error) { + throw handleApiError(error, "update workspace content"); + } +} + +export async function deleteWorkspace( + id: number, +): Promise<{ success: boolean }> { + try { + const response = await authApi.delete(`/workspaces/${id}`); + return response.data; + } catch (error) { + throw handleApiError(error, "delete workspace"); + } +} + +export async function duplicateWorkspace( + id: number, + name: string, +): Promise { + try { + const response = await authApi.post(`/workspaces/${id}/duplicate`, { + name, + }); + return response.data; + } catch (error) { + throw handleApiError(error, "duplicate workspace"); + } +} + +export async function setDefaultWorkspace(id: number): Promise { + try { + const response = await authApi.post(`/workspaces/${id}/set-default`); + return response.data; + } catch (error) { + throw handleApiError(error, "set default workspace"); + } +} + +export async function unsetDefaultWorkspace(id: number): Promise { + try { + const response = await authApi.post(`/workspaces/${id}/unset-default`); + return response.data; + } catch (error) { + throw handleApiError(error, "unset default workspace"); + } +} + +export async function applyWorkspaceServer(id: number): Promise { + try { + const response = await authApi.post(`/workspaces/${id}/apply`); + return response.data; + } catch (error) { + throw handleApiError(error, "apply workspace"); + } +} + +export async function getLastSessionWorkspace(): Promise { + try { + const response = await authApi.get("/workspaces/last-session"); + return response.data; + } catch (error) { + throw handleApiError(error, "fetch last session workspace"); + } +} + +export async function saveLastSessionWorkspace( + payload: WorkspacePayload, +): Promise { + try { + const response = await authApi.put("/workspaces/last-session", { + payload, + }); + return response.data; + } catch (error) { + throw handleApiError(error, "save last session workspace"); + } +} diff --git a/src/ui/auth/Auth.tsx b/src/ui/auth/Auth.tsx index b34a4c4..504bdc4 100644 --- a/src/ui/auth/Auth.tsx +++ b/src/ui/auth/Auth.tsx @@ -29,19 +29,20 @@ import { completePasswordReset, getOIDCAuthorizeUrl, verifyTOTPLogin, - getServerConfig, - saveServerConfig, isElectron, - getEmbeddedServerStatus, getCurrentToken, getOidcSilentLoginDefault, + requestDesktopAutoSession, + requestTrustedProxyLogin, } from "@/main-axios"; import { getSSOProviders, ldapLogin } from "@/api/sso-provider-api"; import type { SSOProviderPublic } from "@/types/index"; -import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig"; -import { ElectronLoginForm } from "@/auth/ElectronLoginForm"; import { Checkbox } from "@/components/checkbox"; -import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n"; +import { + changeAppLanguage, + normalizeLanguageCode, + rememberLoginLanguage, +} from "@/i18n/i18n"; import { removeSilentSigninFromSearch, shouldTriggerSilentSignin, @@ -243,7 +244,8 @@ export function Auth({ onLogin }: AuthProps) { ); function handleLanguageChange(code: string) { - void changeAppLanguage(code) + const language = rememberLoginLanguage(code); + void changeAppLanguage(language) .then((language) => setLanguage(language)) .catch(() => {}); } @@ -257,19 +259,49 @@ export function Auth({ onLogin }: AuthProps) { const [ldapUsername, setLdapUsername] = useState(""); const [ldapPassword, setLdapPassword] = useState(""); const silentSigninHandledRef = useRef(false); + const proxySigninHandledRef = useRef(false); const [oidcSilentLoginDefault, setOidcSilentLoginDefault] = useState(false); const [oidcSilentLoginDefaultLoaded, setOidcSilentLoginDefaultLoaded] = useState(false); const [firstUser, setFirstUser] = useState(false); const [dbConnectionFailed, setDbConnectionFailed] = useState(false); const [dbHealthChecking, setDbHealthChecking] = useState(true); - - const [showServerConfig, setShowServerConfig] = useState( - null, - ); - const [currentServerUrl, setCurrentServerUrl] = useState(""); const [webviewAuthSuccess, setWebviewAuthSuccess] = useState(false); + // Electron, non-iframed only: the desktop app never shows a login form + // when running standalone -- the embedded backend auto-provisions a + // single local user on first boot, and this component silently exchanges + // that for a session instead of rendering login/register. + // null = probe still in flight (Electron only, blocks rendering below). + // true = probe settled with no auto-login (not applicable outside + // Electron, multiple users exist, or setup is genuinely required) -- + // safe to fall through to the normal form/health-check flow. + // Auto-login success never sets this; it calls onLogin directly and this + // component unmounts. + const [desktopAutoSessionDone, setDesktopAutoSessionDone] = useState< + boolean | null + >(!isElectron() || isInElectronWebView() ? true : null); + const [desktopAutoSessionRetries, setDesktopAutoSessionRetries] = useState(0); + + useEffect(() => { + if (proxySigninHandledRef.current || isElectron()) return; + proxySigninHandledRef.current = true; + requestTrustedProxyLogin() + .then((result) => { + if (!result.enabled || !result.success) return; + storeAuth(result.username || ""); + onLogin( + result.username || "", + result.userId || undefined, + !!result.is_admin, + ); + }) + .catch(() => { + // Leave the login screen visible. The backend logs the reason without + // exposing trusted proxy configuration to an untrusted client. + }); + }, [onLogin]); + useEffect(() => { try { localStorage.setItem("rememberMe", rememberMe.toString()); @@ -320,65 +352,106 @@ export function Auth({ onLogin }: AuthProps) { }, []); useEffect(() => { - if (showServerConfig) return; - setDbHealthChecking(true); - getSetupRequired() - .then((res) => { - if (res.setup_required) { - setFirstUser(true); - setView("register"); - } - setDbConnectionFailed(false); - }) - .catch(() => setDbConnectionFailed(true)) - .finally(() => setDbHealthChecking(false)); - }, [showServerConfig]); + // Runs once the auto-session probe has settled (immediately outside + // Electron, since it starts at true there; after the probe resolves in + // Electron). Waiting avoids flashing a login screen the user is about + // to skip past via auto-login. + if (desktopAutoSessionDone !== true) return; + let cancelled = false; + let retryTimer: ReturnType | null = null; - useEffect(() => { - const checkElectron = async () => { - if (isInElectronWebView()) { - setShowServerConfig(false); - return; - } - if (isElectron()) { - const forceShow = localStorage.getItem("termix_show_server_config"); - if (forceShow === "true") { - localStorage.removeItem("termix_show_server_config"); - try { - const config = await getServerConfig(); - setCurrentServerUrl(config?.serverUrl || ""); - } catch { - // ignore + // Right after a server update/restart (or behind a reverse proxy that's + // still warming up), the very first request can transiently fail even + // though the backend/database is fine seconds later. A single failure + // here used to permanently show the "could not connect to the database" + // screen, forcing users to manually reload -- sometimes repeatedly. + // Retry a few times with backoff before treating it as a real failure. + const maxAttempts = 5; + const attempt = (attemptNumber: number) => { + getSetupRequired() + .then((res) => { + if (cancelled) return; + if (res.setup_required) { + setFirstUser(true); + setView("register"); } - setShowServerConfig(true); - return; - } - try { - const [config, status] = await Promise.all([ - getServerConfig(), - getEmbeddedServerStatus(), - ]); - if ( - status?.embedded && - status?.running && - config && - !config.serverUrl - ) { - setShowServerConfig(false); - setCurrentServerUrl(""); + setDbConnectionFailed(false); + setDbHealthChecking(false); + }) + .catch(() => { + if (cancelled) return; + if (attemptNumber >= maxAttempts) { + setDbConnectionFailed(true); + setDbHealthChecking(false); return; } - setCurrentServerUrl(config?.serverUrl || ""); - setShowServerConfig(!config || !config.serverUrl); - } catch { - setShowServerConfig(true); - } - } else { - setShowServerConfig(false); - } + const delay = Math.min(500 * 2 ** attemptNumber, 5000); + retryTimer = setTimeout(() => { + if (!cancelled) attempt(attemptNumber + 1); + }, delay); + }); }; - checkElectron(); - }, []); + + setDbHealthChecking(true); + attempt(0); + + return () => { + cancelled = true; + if (retryTimer) clearTimeout(retryTimer); + }; + }, [desktopAutoSessionDone]); + + // A cold first launch spawns the embedded backend as a separate process + // that can take anywhere from a couple seconds to much longer to finish + // booting (DB init, SSL, antivirus scanning a freshly-unpacked binary, + // slow disks, etc.) -- well after the renderer has already mounted. The + // embedded backend is bundled, always-on infrastructure, not something + // that can be "not there" -- it always eventually comes up. So a + // "retry" outcome (connection error, not a real verdict) is retried + // forever with capped backoff rather than ever giving up and falling + // through to the login form: that form is not a valid destination for a + // standalone install with no remote sync configured, since the only + // local account has no password to log in with. Only a definitive + // "declined" (backend reachable and says no -- multiple users, or the + // sole local user has a real credential) stops retrying and shows the + // real form. + useEffect(() => { + if (desktopAutoSessionDone !== null) return; + let cancelled = false; + let retryTimer: ReturnType | null = null; + requestDesktopAutoSession() + .then((outcome) => { + if (cancelled) return; + if (outcome.kind === "success") { + storeAuth(outcome.data.username || ""); + onLogin( + outcome.data.username || "", + outcome.data.userId || undefined, + !!outcome.data.is_admin, + ); + return; + } + if (outcome.kind === "retry") { + const delay = Math.min(1000 * 2 ** desktopAutoSessionRetries, 10000); + retryTimer = setTimeout(() => { + if (!cancelled) setDesktopAutoSessionRetries((c) => c + 1); + }, delay); + return; + } + setDesktopAutoSessionDone(true); + }) + .catch(() => { + if (cancelled) return; + const delay = Math.min(1000 * 2 ** desktopAutoSessionRetries, 10000); + retryTimer = setTimeout(() => { + if (!cancelled) setDesktopAutoSessionRetries((c) => c + 1); + }, delay); + }); + return () => { + cancelled = true; + if (retryTimer) clearTimeout(retryTimer); + }; + }, [desktopAutoSessionDone, desktopAutoSessionRetries, onLogin]); useEffect(() => { if (view === "totp" && totpInputRef.current) totpInputRef.current.focus(); @@ -474,36 +547,6 @@ export function Auth({ onLogin }: AuthProps) { } }, [onLogin, t]); - const handleElectronAuthSuccess = useCallback( - async (token: string | null) => { - try { - if (!token) { - // No token in postMessage โ€” fall back to waiting for the HttpOnly cookie - const cookieReady = await window.electronAPI?.waitForSessionCookie?.( - "jwt", - currentServerUrl, - null, - 5000, - ); - if (cookieReady && !cookieReady.success) - throw new Error(cookieReady.error || "Auth cookie not ready"); - } - const meRes = await getUserInfo(); - if (!meRes) throw new Error("Failed to get user info"); - storeAuth(meRes.username || ""); - onLogin( - meRes.username || "", - meRes.userId || undefined, - !!meRes.is_admin, - ); - toast.success(t("messages.loginSuccess")); - } catch { - toast.error(t("errors.failedUserInfo")); - } - }, - [onLogin, currentServerUrl, t], - ); - function resetAll() { setUsername(""); setPassword(""); @@ -549,11 +592,19 @@ export function Auth({ onLogin }: AuthProps) { return; } if (isInElectronWebView()) { + // The iframe's login request never carries the X-Electron-App header + // (only the top-level Electron renderer's axios instances do), so the + // backend never includes the JWT in the login response body -- it + // only lands in an HttpOnly cookie scoped to this iframe's origin. + // Read it back via /users/me/token, same as the mobile-webview OIDC + // callback below does, so the parent window can persist it. + const token = res?.token ?? (await getCurrentToken()); window.parent.postMessage( { type: "AUTH_SUCCESS", source: "auth_component", platform: "desktop", + token: token ?? null, timestamp: Date.now(), }, "*", @@ -607,6 +658,35 @@ export function Auth({ onLogin }: AuthProps) { setView("totp"); return; } + if (isInMobileWebView()) { + // Native-app requests get the JWT in the login response body. + const token = res?.token ?? ""; + (window as ExtendedWindow).ReactNativeWebView?.postMessage( + JSON.stringify({ type: "AUTH_SUCCESS", token }), + ); + setWebviewAuthSuccess(true); + return; + } + if (isInElectronWebView()) { + // Registration inside the Remote Sync iframe must hand off to the + // parent window the same way handleLogin does -- otherwise this + // component's own onLogin() below fires on the iframe's own, + // independent copy of the app, rendering the full remote AppShell + // inside the small login dialog instead of closing it. + const token = res?.token ?? (await getCurrentToken()); + window.parent.postMessage( + { + type: "AUTH_SUCCESS", + source: "auth_component", + platform: "desktop", + token: token ?? null, + timestamp: Date.now(), + }, + "*", + ); + setWebviewAuthSuccess(true); + return; + } const meRes = await getUserInfo(); storeAuth(meRes.username || username.trim()); toast.success(t("messages.registrationSuccess")); @@ -650,11 +730,16 @@ export function Auth({ onLogin }: AuthProps) { return; } if (isInElectronWebView()) { + // See the equivalent branch in handleLogin: the iframe never sends + // X-Electron-App, so the JWT never lands in the response body here + // either -- read it back from the HttpOnly cookie that was just set. + const token = res?.token ?? (await getCurrentToken()); window.parent.postMessage( { type: "AUTH_SUCCESS", source: "totp_auth_component", platform: "desktop", + token: token ?? null, timestamp: Date.now(), }, "*", @@ -935,46 +1020,19 @@ export function Auth({ onLogin }: AuthProps) { oidcSilentLoginDefaultLoaded, ]); - // Electron server config / webview auth success screens - if (isElectron() && !isInElectronWebView()) { - if (showServerConfig === null) - return ( -
-
-
- ); - if (showServerConfig) - return ( -
-
- window.location.reload()} - onUseEmbedded={async () => { - await saveServerConfig({ - serverUrl: "", - lastUpdated: new Date().toISOString(), - }); - setShowServerConfig(false); - setCurrentServerUrl(""); - }} - onCancel={() => setShowServerConfig(false)} - isFirstTime={!currentServerUrl} - /> -
-
- ); - if (!webviewAuthSuccess && showServerConfig === false && currentServerUrl) - return ( -
-
- setShowServerConfig(true)} - /> -
-
- ); + // Electron, non-iframed: wait for the auto-session probe before rendering + // anything, so a standalone desktop install never flashes a login form + // it's about to skip past. + if ( + isElectron() && + !isInElectronWebView() && + desktopAutoSessionDone === null + ) { + return ( +
+
+
+ ); } if (webviewAuthSuccess || (isInElectronWebView() && webviewAuthSuccess)) @@ -1018,30 +1076,11 @@ export function Auth({ onLogin }: AuthProps) { ))}
- {isElectron() && currentServerUrl && ( -
-
- - {t("serverConfig.serverUrl")} - - - {currentServerUrl} - -
- -
- )}
); - if (dbHealthChecking && showServerConfig === false) + if (dbHealthChecking) return (
@@ -1068,21 +1107,6 @@ export function Auth({ onLogin }: AuthProps) { return (
- {isElectron() && !isInElectronWebView() && showServerConfig === false && ( -
- - - {t("serverConfig.localServer")} - -
-
- )}
{/* Left decorative panel */}
diff --git a/src/ui/auth/ElectronLoginForm.tsx b/src/ui/auth/ElectronLoginForm.tsx index b49cb35..1a799b2 100644 --- a/src/ui/auth/ElectronLoginForm.tsx +++ b/src/ui/auth/ElectronLoginForm.tsx @@ -1,3 +1,4 @@ +import { getErrorMessage } from "../lib/error-message.js"; import React, { useState, useEffect, useRef, useCallback } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; import { useTranslation } from "react-i18next"; @@ -7,6 +8,18 @@ interface ElectronLoginFormProps { serverUrl: string; onAuthSuccess: (token: string | null) => void | Promise; onChangeServer: () => void; + // "local" (default): the app's own login, JWT goes to localStorage like + // every other client. "remoteSync": this iframe is authenticating a + // Settings-triggered connection to a remote Termix server for the sync + // engine -- the JWT is handed to the Electron main process's encrypted + // store instead, never exposed to the renderer's localStorage. + targetPurpose?: "local" | "remoteSync"; +} + +interface SaveRemoteSyncJwtResult { + success: boolean; + reason?: string; + error?: string; } const AUTH_MESSAGE_SOURCES = new Set([ @@ -19,6 +32,7 @@ export function ElectronLoginForm({ serverUrl, onAuthSuccess, onChangeServer, + targetPurpose = "local", }: ElectronLoginFormProps) { const { t } = useTranslation(); const [loading, setLoading] = useState(true); @@ -43,17 +57,40 @@ export function ElectronLoginForm({ try { if (token) { - localStorage.setItem("jwt", token); + if (targetPurpose === "remoteSync") { + // The main process refuses to persist the JWT when it has no OS + // keyring to encrypt it with, and reports that by resolving with + // success: false. Dropping the result signs the user in against a + // store that kept nothing, so the next sync tick calls a session + // that was never saved expired. + const result = (await window.electronAPI?.invoke?.( + "save-remote-sync-jwt", + token, + )) as SaveRemoteSyncJwtResult | undefined; + if (!result?.success) { + throw new Error( + result?.reason === "encryption_unavailable" + ? t("errors.keyringUnavailable") + : result?.error || t("errors.authTokenSaveFailed"), + ); + } + } else { + localStorage.setItem("jwt", token); + } } await onAuthSuccessRef.current(token); - } catch { - setError(t("errors.authTokenSaveFailed")); + } catch (err) { + setError( + err instanceof Error && err.message + ? err.message + : t("errors.authTokenSaveFailed"), + ); isAuthenticatingRef.current = false; setIsAuthenticating(false); hasAuthenticatedRef.current = false; } }, - [t], + [t, targetPurpose], ); // postMessage from server Auth.tsx after the backend has set the HttpOnly cookie. @@ -140,8 +177,7 @@ export function ElectronLoginForm({ typeof event.data.providerId === "number" ? event.data.providerId : undefined; - const error = - err instanceof Error ? err.message : t("errors.failedOidcLogin"); + const error = getErrorMessage(err, t("errors.failedOidcLogin")); iframeRef.current?.contentWindow?.postMessage( { type: "OIDC_SYSTEM_BROWSER_AUTH_RESULT", @@ -204,7 +240,7 @@ export function ElectronLoginForm({ const isEmbeddedServer = serverUrl.includes("localhost:30001"); return ( -
+
{isAuthenticating && (
@@ -212,7 +248,7 @@ export function ElectronLoginForm({ )} {!isAuthenticating && ( -
+
-

- {t("serverConfig.embeddedDesc")} -

-
-
- - {t("common.or") || "OR"} - -
-
- - )} - -
-
- -
- handleUrlChange(e.target.value)} - disabled={loading || embeddedLoading} - className={savedUrls.length > 0 ? "pr-9" : ""} - onFocus={() => { - if (savedUrls.length > 0) setDropdownOpen(true); - }} - /> - {savedUrls.length > 0 && ( - - )} - {dropdownOpen && savedUrls.length > 0 && ( -
-

- {t("serverConfig.savedServers")} -

- {savedUrls.map((url) => ( -
- - -
- ))} -
- )} -
-
- - {serverUrl.trim().startsWith("https://") && ( -
-
- -

- {t("serverConfig.allowInvalidCertificateDesc")} -

-
- -
- )} - - {error && ( - - {t("common.error")} - {error} - - )} - -
- {onCancel && !isFirstTime && ( - - )} - -
- -

- {t("serverConfig.helpText")} -

-
-
-
- ); -} diff --git a/src/ui/auth/LoginPage.tsx b/src/ui/auth/LoginPage.tsx deleted file mode 100644 index 4efd849..0000000 --- a/src/ui/auth/LoginPage.tsx +++ /dev/null @@ -1,1981 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React, { useState, useEffect, useCallback, useRef } from "react"; -import { Button } from "@/components/button.tsx"; -import { Input } from "@/components/input.tsx"; -import { PasswordInput } from "@/components/password-input.tsx"; -import { Label } from "@/components/label.tsx"; -import { Checkbox } from "@/components/checkbox.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; -import { Tabs, TabsList, TabsTrigger } from "@/components/tabs.tsx"; -import { useTranslation } from "react-i18next"; -import { LanguageSwitcher } from "@/user/LanguageSwitcher.tsx"; -import { toast } from "sonner"; -import { Sun, Moon, Monitor } from "lucide-react"; -import { useTheme } from "@/components/theme-provider"; -import { - registerUser, - loginUser, - getUserInfo, - getRegistrationAllowed, - getPasswordLoginAllowed, - getSetupRequired, - initiatePasswordReset, - verifyPasswordResetCode, - completePasswordReset, - getOIDCAuthorizeUrl, - verifyTOTPLogin, - getServerConfig, - saveServerConfig, - isElectron, - getEmbeddedServerStatus, - getCurrentToken, - getOidcSilentLoginDefault, -} from "@/main-axios"; -import { getSSOProviders, ldapLogin } from "@/api/sso-provider-api"; -import { authenticateWithWebAuthn } from "@/api/webauthn-api"; -import type { SSOProviderPublic } from "@/types/index"; -import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig.tsx"; -import { ElectronLoginForm } from "@/auth/ElectronLoginForm.tsx"; -import { - removeSilentSigninFromSearch, - shouldTriggerSilentSignin, -} from "./silent-signin"; - -interface ExtendedWindow extends Window { - IS_ELECTRON_WEBVIEW?: boolean; - ReactNativeWebView?: { postMessage: (msg: string) => void }; -} - -const isInMobileWebView = () => - /Termix-Mobile\/(Android|iOS)/.test(navigator.userAgent) || - !!(window as ExtendedWindow).ReactNativeWebView; - -interface AuthProps extends React.ComponentProps<"div"> { - setLoggedIn: (loggedIn: boolean) => void; - setIsAdmin: (isAdmin: boolean) => void; - setUsername: (username: string | null) => void; - setUserId: (userId: string | null) => void; - loggedIn: boolean; - authLoading: boolean; - setDbError: (error: string | null) => void; - onAuthSuccess: (authData: { - isAdmin: boolean; - username: string | null; - userId: string | null; - }) => void; -} - -export function Auth({ - className, - setLoggedIn, - setIsAdmin, - setUsername, - setUserId, - loggedIn, - authLoading, - setDbError, - onAuthSuccess, - ...props -}: AuthProps) { - const { t } = useTranslation(); - const { theme, setTheme } = useTheme(); - - const isDarkMode = - theme === "dark" || - theme === "dracula" || - theme === "gentlemansChoice" || - theme === "midnightEspresso" || - theme === "catppuccinMocha" || - (theme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - const lineColor = isDarkMode ? "#151517" : "#f9f9f9"; - - const isInElectronWebView = useCallback(() => { - if (isInMobileWebView()) return false; - if ((window as ExtendedWindow).IS_ELECTRON_WEBVIEW) { - return true; - } - try { - if (window.self !== window.top) { - return true; - } - } catch { - return true; - } - return false; - }, []); - - const [tab, setTab] = useState<"login" | "signup" | "reset">("login"); - const [localUsername, setLocalUsername] = useState(""); - const [password, setPassword] = useState(""); - const [signupConfirmPassword, setSignupConfirmPassword] = useState(""); - const [rememberMe, setRememberMe] = useState(() => { - try { - const saved = localStorage.getItem("rememberMe"); - return saved === "true"; - } catch { - return false; - } - }); - const [loading, setLoading] = useState(false); - const [passkeyLoading, setPasskeyLoading] = useState(false); - const [oidcLoading, setOidcLoading] = useState(false); - const [internalLoggedIn, setInternalLoggedIn] = useState(false); - const [firstUser, setFirstUser] = useState(false); - const [firstUserToastShown, setFirstUserToastShown] = useState(false); - const [registrationAllowed, setRegistrationAllowed] = useState(true); - const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true); - const [ssoProviders, setSsoProviders] = useState([]); - const [ssoProvidersLoaded, setSsoProvidersLoaded] = useState(false); - const [ldapProviderId, setLdapProviderId] = useState(null); - const [ldapUsername, setLdapUsername] = useState(""); - const [ldapPassword, setLdapPassword] = useState(""); - const [ldapLoading, setLdapLoading] = useState(false); - const silentSigninHandledRef = useRef(false); - const [oidcSilentLoginDefault, setOidcSilentLoginDefault] = useState(false); - const [oidcSilentLoginDefaultLoaded, setOidcSilentLoginDefaultLoaded] = - useState(false); - - const [resetStep, setResetStep] = useState< - "initiate" | "verify" | "newPassword" - >("initiate"); - const [resetCode, setResetCode] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [tempToken, setTempToken] = useState(""); - const [resetLoading, setResetLoading] = useState(false); - const [resetSuccess, setResetSuccess] = useState(false); - - const [totpRequired, setTotpRequired] = useState(false); - const [totpCode, setTotpCode] = useState(""); - const [totpTempToken, setTotpTempToken] = useState(""); - const [totpLoading, setTotpLoading] = useState(false); - const [webviewAuthSuccess, setWebviewAuthSuccess] = useState(false); - const totpInputRef = React.useRef(null); - - // Hand the JWT to the native app embedding this page in a React Native WebView. - // The mobile onMessage handler only reads { type, token }. - const postMobileAuthSuccess = useCallback((token: string) => { - (window as ExtendedWindow).ReactNativeWebView?.postMessage( - JSON.stringify({ type: "AUTH_SUCCESS", token }), - ); - setWebviewAuthSuccess(true); - }, []); - - const [showServerConfig, setShowServerConfig] = useState( - null, - ); - const [currentServerUrl, setCurrentServerUrl] = useState(""); - const [dbConnectionFailed, setDbConnectionFailed] = useState(false); - const [dbHealthChecking, setDbHealthChecking] = useState(false); - - const handleElectronAuthSuccess = useCallback(async () => { - try { - // token was stored in localStorage by ElectronLoginForm before this runs, - // so getUserInfo() can authenticate via the cookie interceptor or localStorage jwt. - let retries = 5; - let meRes = null; - while (retries-- > 0) { - try { - meRes = await getUserInfo(); - break; - } catch (err: unknown) { - const isNoServer = - (err as { code?: string })?.code === "NO_SERVER_CONFIGURED" || - (err as Error)?.message?.includes("no-server-configured"); - if (isNoServer && retries > 0) { - await new Promise((r) => setTimeout(r, 500)); - } else { - throw err; - } - } - } - if (!meRes) throw new Error("Failed to get user info"); - setInternalLoggedIn(true); - setLoggedIn(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - setUserId(meRes.userId || null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - toast.success(t("messages.loginSuccess")); - } catch { - toast.error(t("errors.failedUserInfo")); - } - }, [ - onAuthSuccess, - setLoggedIn, - setIsAdmin, - setUsername, - setUserId, - t, - setInternalLoggedIn, - ]); - - useEffect(() => { - setInternalLoggedIn(loggedIn); - }, [loggedIn]); - - useEffect(() => { - if (totpRequired && totpInputRef.current) { - totpInputRef.current.focus(); - } - }, [totpRequired]); - - useEffect(() => { - try { - localStorage.setItem("rememberMe", rememberMe.toString()); - } catch { - // expected - localStorage might not be available - } - }, [rememberMe]); - - useEffect(() => { - getRegistrationAllowed().then((res) => { - setRegistrationAllowed(res.allowed); - }); - }, [isInElectronWebView]); - - useEffect(() => { - getPasswordLoginAllowed() - .then((res) => { - setPasswordLoginAllowed(res.allowed); - }) - .catch((err) => { - if (err.code !== "NO_SERVER_CONFIGURED") { - console.error("Failed to fetch password login status:", err); - } - }); - }, []); - - useEffect(() => { - getSSOProviders() - .then((providers) => { - setSsoProviders(providers || []); - }) - .catch(() => { - setSsoProviders([]); - }) - .finally(() => { - setSsoProvidersLoaded(true); - }); - }, []); - - useEffect(() => { - getOidcSilentLoginDefault() - .then((res) => { - setOidcSilentLoginDefault(res.enabled); - }) - .catch(() => {}) - .finally(() => { - setOidcSilentLoginDefaultLoaded(true); - }); - }, []); - - useEffect(() => { - if (showServerConfig) { - return; - } - - setDbHealthChecking(true); - getSetupRequired() - .then((res) => { - if (res.setup_required) { - setFirstUser(true); - setTab("signup"); - if (!firstUserToastShown) { - toast.info(t("auth.firstUserMessage")); - setFirstUserToastShown(true); - } - } else { - setFirstUser(false); - } - setDbError(null); - setDbConnectionFailed(false); - }) - .catch(() => { - setDbConnectionFailed(true); - }) - .finally(() => { - setDbHealthChecking(false); - }); - }, [setDbError, firstUserToastShown, showServerConfig, t]); - - // When password login is disabled and SSO is available, stay on login tab - // (SSO buttons appear below the form regardless of tab) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setLoading(true); - - if (!localUsername.trim()) { - toast.error(t("errors.requiredField")); - setLoading(false); - return; - } - - if (!passwordLoginAllowed && !firstUser) { - toast.error(t("errors.passwordLoginDisabled")); - setLoading(false); - return; - } - - try { - let res; - if (tab === "login") { - res = await loginUser(localUsername, password, rememberMe); - } else { - if (password !== signupConfirmPassword) { - toast.error(t("errors.passwordMismatch")); - setLoading(false); - return; - } - if (password.length < 6) { - toast.error(t("errors.minLength", { min: 6 })); - setLoading(false); - return; - } - - await registerUser(localUsername, password); - res = await loginUser(localUsername, password, rememberMe); - } - - if (res.requires_totp) { - setTotpRequired(true); - setTotpTempToken(res.temp_token); - setLoading(false); - return; - } - - if (!res || !res.success) { - throw new Error(t("errors.loginFailed")); - } - - if (isInMobileWebView()) { - // Native-app requests get the JWT in the login response body. - postMobileAuthSuccess(res.token || ""); - return; - } - - if (isInElectronWebView()) { - try { - window.parent.postMessage( - { - type: "AUTH_SUCCESS", - source: "auth_component", - platform: "desktop", - token: res.token || null, - timestamp: Date.now(), - }, - "*", - ); - setWebviewAuthSuccess(true); - return; - } catch (e) { - console.error("Error posting auth success message:", e); - } - } - - const [meRes] = await Promise.all([getUserInfo()]); - - setInternalLoggedIn(true); - setLoggedIn(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - setUserId(meRes.userId || null); - setDbError(null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - setInternalLoggedIn(true); - if (tab === "signup") { - setSignupConfirmPassword(""); - toast.success(t("messages.registrationSuccess")); - } else { - toast.success(t("messages.loginSuccess")); - } - setTotpRequired(false); - setTotpCode(""); - setTotpTempToken(""); - } catch (err: unknown) { - const error = err as { - message?: string; - response?: { data?: { error?: string } }; - }; - const errorMessage = - error?.response?.data?.error || - error?.message || - t("errors.unknownError"); - toast.error(errorMessage); - setInternalLoggedIn(false); - setLoggedIn(false); - setIsAdmin(false); - setUsername(null); - setUserId(null); - if (error?.response?.data?.error?.includes("Database")) { - setDbConnectionFailed(true); - } else { - setDbError(null); - } - } finally { - setLoading(false); - } - } - - async function handleInitiatePasswordReset() { - setResetLoading(true); - try { - await initiatePasswordReset(localUsername); - setResetStep("verify"); - toast.success(t("messages.resetCodeSent")); - } catch (err: unknown) { - const error = err as { - message?: string; - response?: { data?: { error?: string } }; - }; - toast.error( - error?.response?.data?.error || - error?.message || - t("errors.failedPasswordReset"), - ); - } finally { - setResetLoading(false); - } - } - - async function handleVerifyResetCode() { - setResetLoading(true); - try { - const response = await verifyPasswordResetCode(localUsername, resetCode); - setTempToken(response.tempToken); - setResetStep("newPassword"); - toast.success(t("messages.codeVerified")); - } catch (err: unknown) { - const error = err as { - response?: { - data?: { - error?: string; - code?: string; - remainingTime?: number; - remainingAttempts?: number; - }; - }; - }; - const errorCode = error?.response?.data?.code; - const remainingTime = error?.response?.data?.remainingTime; - const remainingAttempts = error?.response?.data?.remainingAttempts; - - let errorMessage = - error?.response?.data?.error || t("errors.failedVerifyCode"); - - if (errorCode === "RESET_CODE_RATE_LIMITED") { - if (remainingTime) { - errorMessage = t("errors.resetCodeRateLimitedWithTime", { - time: remainingTime, - }); - } else { - errorMessage = t("errors.resetCodeRateLimited"); - } - } else if ( - remainingAttempts !== undefined && - remainingAttempts <= 2 && - remainingAttempts > 0 - ) { - errorMessage = `${errorMessage} (${remainingAttempts} ${t("auth.attemptsRemaining")})`; - } - - toast.error(errorMessage); - } finally { - setResetLoading(false); - } - } - - async function handleCompletePasswordReset() { - setResetLoading(true); - - if (newPassword !== confirmPassword) { - toast.error(t("errors.passwordMismatch")); - setResetLoading(false); - return; - } - - if (newPassword.length < 6) { - toast.error(t("errors.minLength", { min: 6 })); - setResetLoading(false); - return; - } - - try { - try { - await completePasswordReset(localUsername, tempToken, newPassword); - } catch (err: unknown) { - const error = err as { - response?: { data?: { code?: string } }; - }; - if (error?.response?.data?.code !== "DATA_WIPE_REQUIRED") { - throw err; - } - if (!window.confirm(t("auth.confirmResetDataWipe"))) { - setResetLoading(false); - return; - } - await completePasswordReset( - localUsername, - tempToken, - newPassword, - true, - ); - } - - setResetStep("initiate"); - setResetCode(""); - setNewPassword(""); - setConfirmPassword(""); - setTempToken(""); - - setResetSuccess(true); - toast.success(t("messages.passwordResetSuccess")); - - setTab("login"); - resetPasswordState(); - } catch (err: unknown) { - const error = err as { response?: { data?: { error?: string } } }; - toast.error( - error?.response?.data?.error || t("errors.failedCompleteReset"), - ); - } finally { - setResetLoading(false); - } - } - - function resetPasswordState() { - setResetStep("initiate"); - setResetCode(""); - setNewPassword(""); - setConfirmPassword(""); - setTempToken(""); - setResetSuccess(false); - setSignupConfirmPassword(""); - } - - function clearFormFields() { - setPassword(""); - setSignupConfirmPassword(""); - } - - async function handleTOTPVerification() { - if (totpCode.length !== 6) { - toast.error(t("auth.enterCode")); - return; - } - - setTotpLoading(true); - - try { - const res = await verifyTOTPLogin(totpTempToken, totpCode, rememberMe); - - if (!res || !res.success) { - throw new Error(t("errors.loginFailed")); - } - - if (isInMobileWebView()) { - // Native-app requests get the JWT in the verify response body. - postMobileAuthSuccess(res.token || ""); - setTotpLoading(false); - return; - } - - if (isInElectronWebView()) { - try { - window.parent.postMessage( - { - type: "AUTH_SUCCESS", - source: "totp_auth_component", - platform: "desktop", - token: res.token || null, - timestamp: Date.now(), - }, - "*", - ); - setWebviewAuthSuccess(true); - setTotpLoading(false); - return; - } catch (e) { - console.error("Error posting auth success message:", e); - } - } - - setLoggedIn(true); - setIsAdmin(!!res.is_admin); - setUsername(res.username || null); - setUserId(res.userId || null); - setDbError(null); - - onAuthSuccess({ - isAdmin: !!res.is_admin, - username: res.username || null, - userId: res.userId || null, - }); - - setInternalLoggedIn(true); - setTotpRequired(false); - setTotpCode(""); - setTotpTempToken(""); - toast.success(t("messages.loginSuccess")); - } catch (err: unknown) { - const error = err as { - message?: string; - response?: { - data?: { - code?: string; - error?: string; - remainingTime?: number; - remainingAttempts?: number; - }; - }; - }; - const errorCode = error?.response?.data?.code; - const remainingTime = error?.response?.data?.remainingTime; - const remainingAttempts = error?.response?.data?.remainingAttempts; - - let errorMessage = - error?.response?.data?.error || - error?.message || - t("errors.invalidTotpCode"); - - if (errorCode === "SESSION_EXPIRED") { - setTotpRequired(false); - setTotpCode(""); - setTotpTempToken(""); - setTab("login"); - toast.error(t("errors.sessionExpired")); - } else if (errorCode === "TOTP_RATE_LIMITED") { - if (remainingTime) { - errorMessage = t("errors.totpRateLimitedWithTime", { - time: remainingTime, - }); - } else { - errorMessage = t("errors.totpRateLimited"); - } - toast.error(errorMessage); - } else { - if ( - remainingAttempts !== undefined && - remainingAttempts <= 2 && - remainingAttempts > 0 - ) { - errorMessage = `${errorMessage} (${remainingAttempts} ${t("auth.attemptsRemaining")})`; - } - toast.error(errorMessage); - } - } finally { - setTotpLoading(false); - } - } - - async function handlePasskeyLogin() { - setPasskeyLoading(true); - try { - const res = await authenticateWithWebAuthn( - localUsername, - rememberMe, - "preferred", - ); - - if (res.requires_totp) { - setTotpRequired(true); - setTotpTempToken(res.temp_token || ""); - return; - } - - if (!res || !res.success) { - throw new Error(t("errors.loginFailed")); - } - - if (isInMobileWebView()) { - postMobileAuthSuccess(res.token || ""); - return; - } - - if (isInElectronWebView()) { - window.parent.postMessage( - { - type: "AUTH_SUCCESS", - source: "passkey_auth_component", - platform: "desktop", - token: res.token || null, - timestamp: Date.now(), - }, - "*", - ); - setWebviewAuthSuccess(true); - return; - } - - const meRes = await getUserInfo(); - setInternalLoggedIn(true); - setLoggedIn(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - setUserId(meRes.userId || null); - setDbError(null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - toast.success(t("messages.loginSuccess")); - } catch (err: unknown) { - const error = err as { - message?: string; - response?: { data?: { error?: string } }; - }; - toast.error( - error?.response?.data?.error || - error?.message || - t("auth.passkeyLoginFailed"), - ); - } finally { - setPasskeyLoading(false); - } - } - - const handleOIDCLogin = useCallback( - async (providerId?: number) => { - setOidcLoading(true); - try { - const authResponse = await getOIDCAuthorizeUrl( - rememberMe, - undefined, - providerId, - ); - const { auth_url: authUrl } = authResponse; - - if (!authUrl || authUrl === "undefined") { - throw new Error(t("errors.invalidAuthUrl")); - } - - window.location.replace(authUrl); - } catch (err: unknown) { - const error = err as { - message?: string; - response?: { data?: { error?: string } }; - }; - const errorMessage = - error?.response?.data?.error || - error?.message || - t("errors.failedOidcLogin"); - toast.error(errorMessage); - setOidcLoading(false); - } - }, - [rememberMe, t], - ); - - const handleLDAPLogin = useCallback( - async (providerId: number) => { - if (!ldapUsername.trim() || !ldapPassword) { - toast.error(t("errors.requiredField")); - return; - } - setLdapLoading(true); - try { - await ldapLogin(providerId, ldapUsername, ldapPassword, rememberMe); - const meRes = await getUserInfo(); - setInternalLoggedIn(true); - setLoggedIn(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - setUserId(meRes.userId || null); - setDbError(null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - toast.success(t("messages.loginSuccess")); - } catch (err: unknown) { - const error = err as { - response?: { data?: { error?: string } }; - message?: string; - }; - toast.error( - error?.response?.data?.error || - error?.message || - t("auth.ldapLoginFailed"), - ); - } finally { - setLdapLoading(false); - } - }, - [ - ldapUsername, - ldapPassword, - rememberMe, - onAuthSuccess, - setLoggedIn, - setIsAdmin, - setUsername, - setUserId, - setDbError, - t, - ], - ); - - useEffect(() => { - if (!ssoProvidersLoaded || silentSigninHandledRef.current) return; - if (!oidcSilentLoginDefaultLoaded) return; - - const urlTriggered = shouldTriggerSilentSignin(window.location.search); - if (!urlTriggered && !oidcSilentLoginDefault) return; - - if (urlTriggered) { - const nextSearch = removeSilentSigninFromSearch(window.location.search); - window.history.replaceState( - {}, - document.title, - `${window.location.pathname}${nextSearch}${window.location.hash}`, - ); - } - - silentSigninHandledRef.current = true; - const oidcProvider = ssoProviders.find( - (p) => p.type === "oidc" || p.type === "github" || p.type === "google", - ); - if (oidcProvider && !isElectron()) { - handleOIDCLogin(oidcProvider.id); - return; - } - - if (ssoProviders.length > 0 && !isElectron()) { - const first = ssoProviders[0]; - if (first.type !== "ldap") handleOIDCLogin(first.id); - return; - } - - if (urlTriggered) { - toast.info(t("errors.silentSigninOidcUnavailable")); - } - }, [ - handleOIDCLogin, - ssoProvidersLoaded, - ssoProviders, - t, - oidcSilentLoginDefault, - oidcSilentLoginDefaultLoaded, - ]); - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const success = urlParams.get("success"); - const error = urlParams.get("error"); - - if (error) { - if (error === "registration_disabled") { - toast.error(t("messages.registrationDisabled")); - } else if (error === "user_not_allowed") { - toast.error(t("messages.userNotAllowed")); - } else { - toast.error(`${t("errors.oidcAuthFailed")}: ${error}`); - } - setOidcLoading(false); - window.history.replaceState({}, document.title, window.location.pathname); - return; - } - - if (success) { - setOidcLoading(true); - - if (isInMobileWebView()) { - // The OIDC callback authenticated via an HttpOnly cookie on this origin, - // so prefer a token in the URL (termix-mobile:-origin callbacks include - // one), otherwise read it back from the cookie via /users/me/token. - const finish = (token: string) => { - postMobileAuthSuccess(token); - setOidcLoading(false); - window.history.replaceState( - {}, - document.title, - window.location.pathname, - ); - }; - const urlToken = urlParams.get("token"); - if (urlToken) { - finish(urlToken); - } else { - getCurrentToken() - .then((token) => finish(token ?? "")) - .catch(() => finish("")); - } - return; - } - - if (isInElectronWebView()) { - try { - const urlToken = urlParams.get("token"); - window.parent.postMessage( - { - type: "AUTH_SUCCESS", - source: "oidc_callback", - platform: "desktop", - token: urlToken || null, - timestamp: Date.now(), - }, - "*", - ); - setWebviewAuthSuccess(true); - setOidcLoading(false); - window.history.replaceState( - {}, - document.title, - window.location.pathname, - ); - return; - } catch (e) { - console.error("Error posting auth success message:", e); - } - } - - getUserInfo() - .then((meRes) => { - setInternalLoggedIn(true); - setLoggedIn(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - setUserId(meRes.userId || null); - setDbError(null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - setInternalLoggedIn(true); - window.history.replaceState( - {}, - document.title, - window.location.pathname, - ); - }) - .catch((err) => { - console.error("Failed to get user info after OIDC callback:", err); - toast.error(t("errors.failedUserInfo")); - setInternalLoggedIn(false); - setLoggedIn(false); - setIsAdmin(false); - setUsername(null); - setUserId(null); - window.history.replaceState( - {}, - document.title, - window.location.pathname, - ); - }) - .finally(() => { - setOidcLoading(false); - }); - } - }, [ - onAuthSuccess, - setDbError, - setIsAdmin, - setLoggedIn, - setUserId, - setUsername, - t, - isInElectronWebView, - ]); - - const Spinner = ( - - - - - ); - - useEffect(() => { - if (dbConnectionFailed) { - toast.error(t("errors.databaseConnection")); - } - }, [dbConnectionFailed, t]); - - useEffect(() => { - const checkServerConfig = async () => { - if (isInElectronWebView()) { - setShowServerConfig(false); - return; - } - - if (isElectron()) { - try { - const [config, status] = await Promise.all([ - getServerConfig(), - getEmbeddedServerStatus(), - ]); - - if ( - status?.embedded && - status?.running && - config && - !config.serverUrl - ) { - setCurrentServerUrl(""); - setShowServerConfig(false); - return; - } - - setCurrentServerUrl(config?.serverUrl || ""); - setShowServerConfig(!config || !config.serverUrl); - } catch { - setShowServerConfig(true); - } - } else { - setShowServerConfig(false); - } - }; - - checkServerConfig(); - }, []); - - if (showServerConfig === null && !isInElectronWebView()) { - return ( -
-
-
-
-
- ); - } - - if (showServerConfig && !isInElectronWebView()) { - return ( -
- { - window.location.reload(); - }} - onUseEmbedded={async () => { - await saveServerConfig({ - serverUrl: "", - lastUpdated: new Date().toISOString(), - }); - setShowServerConfig(false); - setCurrentServerUrl(""); - }} - onCancel={() => { - setShowServerConfig(false); - }} - isFirstTime={!currentServerUrl} - /> -
- ); - } - - if ( - isElectron() && - currentServerUrl && - authLoading && - !isInElectronWebView() - ) { - return ( -
-
-
-
-
-

- {t("common.checkingAuthentication")} -

-
-
-
-
- ); - } - - if (isElectron() && currentServerUrl && !loggedIn && !isInElectronWebView()) { - return ( -
-
- { - setShowServerConfig(true); - }} - /> -
-
- ); - } - - if (dbHealthChecking && !dbConnectionFailed) { - return ( -
-
-
-
-
-

- {t("common.checkingDatabase")} -

-
-
-
-
- ); - } - - if (dbConnectionFailed) { - return ( -
-
-
-

- {t("errors.databaseConnection")} -

-

- {t("messages.databaseConnectionFailed")} -

-
- -
- -
- -
-
- - -
- {isElectron() && currentServerUrl && ( -
-
- -
- {currentServerUrl} -
-
- -
- )} -
-
-
- ); - } - - return ( -
-
-
-
-
- {t("common.appName").toUpperCase()} -
-
- {t("auth.tagline")} -
-
-
- -
-
- {isInElectronWebView() && !webviewAuthSuccess && ( - - - {t("auth.desktopApp")} - - {t("auth.loggingInToDesktopApp")} - - - )} - {(isInElectronWebView() || isInMobileWebView()) && - webviewAuthSuccess && ( -
-
-

- {t("messages.loginSuccess")} -

-

- {t("auth.redirectingToApp")} -

-
-
- )} - {!webviewAuthSuccess && totpRequired && ( -
{ - e.preventDefault(); - handleTOTPVerification(); - }} - > -
-

- {t("auth.twoFactorAuth")} -

-

{t("auth.enterCode")}

-
- -
- - - setTotpCode(e.target.value.replace(/\D/g, "")) - } - disabled={totpLoading} - className="text-center text-2xl tracking-widest font-mono" - autoComplete="one-time-code" - /> -

- {t("auth.backupCode")} -

-
- - - - - - )} - - {!webviewAuthSuccess && - !loggedIn && - !authLoading && - !totpRequired && ( - <> - {(() => { - const hasLogin = passwordLoginAllowed && !firstUser; - const hasSignup = - (passwordLoginAllowed || firstUser) && - registrationAllowed; - const hasPasskey = !firstUser; - const hasSso = ssoProviders.length > 0; - const hasAnyAuth = - hasLogin || hasSignup || hasPasskey || hasSso; - - if (!hasAnyAuth) { - return ( -
-

- {t("auth.authenticationDisabled")} -

-

- {t("auth.authenticationDisabledDesc")} -

-
- ); - } - - return ( - <> - { - const newTab = v as "login" | "signup" | "reset"; - setTab(newTab); - if (tab === "reset") resetPasswordState(); - if ( - (tab === "login" && newTab === "signup") || - (tab === "signup" && newTab === "login") - ) { - clearFormFields(); - } - }} - className="w-full mb-8" - > - - {passwordLoginAllowed && ( - - {t("common.login")} - - )} - {(passwordLoginAllowed || firstUser) && - registrationAllowed && ( - - {t("common.register")} - - )} - - - -
-

- {tab === "login" - ? t("auth.loginTitle") - : tab === "signup" - ? t("auth.registerTitle") - : t("auth.forgotPassword")} -

-
- - {tab === "reset" ? ( -
- {resetStep === "initiate" && ( - <> - - {t("common.warning")} - - {t("auth.dataLossWarning")} - - -
-

{t("auth.resetCodeDesc")}

-
-
-
- - - setLocalUsername(e.target.value) - } - disabled={resetLoading} - /> -
- -
- - )} - - {resetStep === "verify" && ( - <> -
-

- {t("auth.enterResetCode")}{" "} - {localUsername} -

-
-
-
- - - setResetCode( - e.target.value.replace(/\D/g, ""), - ) - } - disabled={resetLoading} - placeholder="000000" - /> -
- - -
- - )} - - {resetStep === "newPassword" && !resetSuccess && ( - <> -
-

- {t("auth.enterNewPassword")}{" "} - {localUsername} -

-
-
-
- - - setNewPassword(e.target.value) - } - disabled={resetLoading} - autoComplete="new-password" - /> -
-
- - - setConfirmPassword(e.target.value) - } - disabled={resetLoading} - autoComplete="new-password" - /> -
- - -
- - )} -
- ) : ( -
- {!passwordLoginAllowed && - !firstUser && - tab === "login" ? ( -
-

- {t("auth.passwordLoginDisabledDesc")} -

-
- - - setLocalUsername(e.target.value) - } - disabled={passkeyLoading || loggedIn} - autoComplete="username webauthn" - /> -
- -
- ) : ( - <> -
- - - setLocalUsername(e.target.value) - } - disabled={loading || loggedIn} - autoComplete="username webauthn" - /> -
-
- - - setPassword(e.target.value) - } - disabled={loading || loggedIn} - /> -
- {tab === "login" && ( -
- - setRememberMe(checked === true) - } - disabled={loading || loggedIn} - /> - -
- )} - {tab === "signup" && ( -
- - - setSignupConfirmPassword(e.target.value) - } - disabled={loading || loggedIn} - /> -
- )} - - {tab === "login" && ( - - )} - {tab === "login" && ( - - )} - - )} - - {ssoProviders.length > 0 && !isElectron() && ( -
- {(passwordLoginAllowed || - firstUser || - tab === "signup") && ( -
-
- - {t("auth.orContinueWith")} - -
-
- )} - {ssoProviders.map((provider) => { - if (provider.type === "ldap") { - const isExpanded = - ldapProviderId === provider.id; - return ( -
- - {isExpanded && ( -
- - setLdapUsername(e.target.value) - } - className="h-9 text-sm" - disabled={ldapLoading} - /> - - setLdapPassword(e.target.value) - } - className="h-9 text-sm" - disabled={ldapLoading} - onKeyDown={(e) => { - if (e.key === "Enter") - handleLDAPLogin(provider.id); - }} - /> - -
- )} -
- ); - } - return ( - - ); - })} -
- )} - - )} - -
-
- - -
- {isElectron() && currentServerUrl && ( -
-
- -
- {currentServerUrl} -
-
- -
- )} -
- - ); - })()} - - )} -
-
-
-
- ); -} diff --git a/src/ui/auth/LoginScreen.tsx b/src/ui/auth/LoginScreen.tsx deleted file mode 100644 index 9fcf715..0000000 --- a/src/ui/auth/LoginScreen.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useState } from "react"; -import { Auth } from "@/auth/LoginPage.tsx"; - -interface LoginScreenProps { - authLoading: boolean; - onAuthSuccess: (authData: { - isAdmin: boolean; - username: string | null; - userId: string | null; - }) => void; -} - -export function LoginScreen({ - authLoading, - onAuthSuccess, -}: LoginScreenProps): React.ReactElement { - const [loggedIn, setLoggedIn] = useState(false); - const [, setIsAdmin] = useState(false); - const [, setUsername] = useState(null); - const [, setUserId] = useState(null); - const [, setDbError] = useState(null); - - return ( -
- -
- ); -} diff --git a/src/ui/components/MigrationNoticeDialog.tsx b/src/ui/components/MigrationNoticeDialog.tsx new file mode 100644 index 0000000..1927855 --- /dev/null +++ b/src/ui/components/MigrationNoticeDialog.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Server } from "lucide-react"; +import { isElectron } from "@/lib/electron"; +import { Button } from "@/components/button.tsx"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/dialog.tsx"; + +type DesktopSettings = { + defaultConnectionOrigin: "local" | "remote"; + migrationNoticeAcknowledged?: boolean; +}; + +// One-time notice for Electron installs that previously pointed the whole +// app at a remote Termix server (the pre-2.6.0 architecture). That install +// now runs a fully local, standalone backend by default -- the hosts, +// credentials, and snippets that lived on the old remote server won't show +// up here until the user explicitly turns on Remote Sync and reconnects to +// that same server. A fresh install never had a legacy serverUrl, so this +// never fires for anyone who didn't go through the old flow, and it only +// ever shows once per install (tracked in desktop-settings.json). +export function MigrationNoticeDialog({ + onOpenRemoteSync, +}: { + onOpenRemoteSync: (serverUrl: string) => void; +}) { + const { t } = useTranslation(); + const [legacyServerUrl, setLegacyServerUrl] = useState(null); + const [open, setOpen] = useState(false); + + useEffect(() => { + if (!isElectron()) return; + let cancelled = false; + + Promise.all([ + window.electronAPI?.invoke?.("get-legacy-server-config") as Promise<{ + serverUrl: string | null; + } | null>, + window.electronAPI?.invoke?.("get-desktop-settings") as Promise< + DesktopSettings | undefined + >, + ]) + .then(([legacyConfig, settings]) => { + if (cancelled) return; + const serverUrl = legacyConfig?.serverUrl || null; + if (serverUrl && !settings?.migrationNoticeAcknowledged) { + setLegacyServerUrl(serverUrl); + setOpen(true); + } + }) + .catch(() => {}); + + return () => { + cancelled = true; + }; + }, []); + + const acknowledge = async () => { + setOpen(false); + const settings = ((await window.electronAPI?.invoke?.( + "get-desktop-settings", + )) as DesktopSettings | undefined) ?? { defaultConnectionOrigin: "local" }; + await window.electronAPI?.invoke?.("save-desktop-settings", { + ...settings, + migrationNoticeAcknowledged: true, + }); + }; + + const handleSetUpSync = async () => { + const url = legacyServerUrl || ""; + await acknowledge(); + onOpenRemoteSync(url); + }; + + if (!legacyServerUrl) return null; + + return ( + !next && acknowledge()}> + + +
+ + {t("migrationNotice.title")} +
+ +

{t("migrationNotice.body1")}

+

{t("migrationNotice.body2", { url: legacyServerUrl })}

+
+
+ + + + +
+
+ ); +} diff --git a/src/ui/components/RemoteSyncBanner.tsx b/src/ui/components/RemoteSyncBanner.tsx new file mode 100644 index 0000000..f554ac4 --- /dev/null +++ b/src/ui/components/RemoteSyncBanner.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AlertTriangle } from "lucide-react"; +import { isElectron } from "@/lib/electron"; + +interface RemoteSyncStatus { + connected: boolean; + syncing: boolean; + lastSyncedAt: string | null; + lastError: string | null; + needsReauth: boolean; +} + +// Non-blocking banner shown when a connected remote sync server needs +// re-authentication. Never gates or hides any other UI -- the local app +// keeps working fully regardless of remote sync state. +export function RemoteSyncBanner({ onReconnect }: { onReconnect: () => void }) { + const { t } = useTranslation(); + const [status, setStatus] = useState(null); + + useEffect(() => { + if (!isElectron()) return; + window.electronAPI + ?.invoke?.("get-remote-sync-status") + .then((s) => setStatus((s as RemoteSyncStatus) ?? null)) + .catch(() => {}); + const unsubscribe = window.electronAPI?.onRemoteSyncStatusChanged?.( + (nextStatus: RemoteSyncStatus) => setStatus(nextStatus), + ); + return () => unsubscribe?.(); + }, []); + + if (!status?.connected || !status.needsReauth) return null; + + return ( +
+
+ + {t("remoteSync.bannerMessage")} +
+ +
+ ); +} diff --git a/src/ui/components/SnippetVariablesDialog.tsx b/src/ui/components/SnippetVariablesDialog.tsx new file mode 100644 index 0000000..3d19387 --- /dev/null +++ b/src/ui/components/SnippetVariablesDialog.tsx @@ -0,0 +1,104 @@ +import { useState, useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/button"; +import { Input } from "@/components/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/dialog"; +import { + extractSnippetInputs, + resolveSnippetContent, + type SnippetHostContext, +} from "@/lib/snippet-variables"; +import type { Snippet } from "@/types/ui-types"; + +/** + * Shown before running a snippet that contains $INPUT_n placeholders -- + * collects a value per placeholder and previews the fully resolved command + * (host vars + inputs) before handing the result back to the caller. + */ +export function SnippetVariablesDialog({ + snippet, + host, + onCancel, + onConfirm, +}: { + snippet: Snippet; + host: SnippetHostContext | null; + onCancel: () => void; + onConfirm: ( + resolvedContent: string, + inputValues: Record, + ) => void; +}) { + const { t } = useTranslation(); + const inputs = useMemo( + () => extractSnippetInputs(snippet.content), + [snippet.content], + ); + const [values, setValues] = useState>({}); + + useEffect(() => { + setValues({}); + }, [snippet]); + + const preview = resolveSnippetContent(snippet.content, host, values); + + return ( + !v && onCancel()}> + + + + {t("newUi.sidebar.snippets.variablesDialogTitle", { + name: snippet.name, + })} + + + {t("newUi.sidebar.snippets.variablesDialogDescription")} + + +
+ {inputs.map((input) => ( +
+ + + setValues((prev) => ({ + ...prev, + [input.key]: e.target.value, + })) + } + /> +
+ ))} +
+ + + {preview} + +
+
+
+ + +
+
+
+ ); +} diff --git a/src/ui/components/accordion.tsx b/src/ui/components/accordion.tsx deleted file mode 100644 index 720bff5..0000000 --- a/src/ui/components/accordion.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDownIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Accordion({ - ...props -}: React.ComponentProps) { - return ; -} - -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AccordionTrigger({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - svg]:rotate-180", - className, - )} - {...props} - > - {children} - - - - ); -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - -
{children}
-
- ); -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/ui/components/alert-dialog.tsx b/src/ui/components/alert-dialog.tsx index dc04cf8..b4edf72 100644 --- a/src/ui/components/alert-dialog.tsx +++ b/src/ui/components/alert-dialog.tsx @@ -52,7 +52,7 @@ function AlertDialogContent({ svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + "relative w-full border border-border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { diff --git a/src/ui/components/badge.tsx b/src/ui/components/badge.tsx index dbc4719..f0ab059 100644 --- a/src/ui/components/badge.tsx +++ b/src/ui/components/badge.tsx @@ -6,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + "inline-flex items-center justify-center rounded-none border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", { variants: { variant: { @@ -17,7 +17,7 @@ const badgeVariants = cva( destructive: "border-transparent bg-destructive text-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, }, defaultVariants: { diff --git a/src/ui/components/button-group.tsx b/src/ui/components/button-group.tsx deleted file mode 100644 index e742659..0000000 --- a/src/ui/components/button-group.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { - Children, - type ReactElement, - cloneElement, - isValidElement, -} from "react"; - -import { type ButtonProps } from "@/components/button"; -import { cn } from "@/lib/utils"; - -interface ButtonGroupProps { - className?: string; - orientation?: "horizontal" | "vertical"; - children: ReactElement[] | React.ReactNode; -} - -export const ButtonGroup = ({ - className, - orientation = "horizontal", - children, -}: ButtonGroupProps) => { - const isHorizontal = orientation === "horizontal"; - const isVertical = orientation === "vertical"; - - // Normalize and filter only valid React elements - const childArray = Children.toArray(children).filter( - (child): child is ReactElement => isValidElement(child), - ); - const totalButtons = childArray.length; - - return ( -
- {childArray.map((child, index) => { - const isFirst = index === 0; - const isLast = index === totalButtons - 1; - - return cloneElement(child, { - className: cn( - { - "rounded-l-none": isHorizontal && !isFirst, - "rounded-r-none": isHorizontal && !isLast, - "border-l-0": isHorizontal && !isFirst, - - "rounded-t-none": isVertical && !isFirst, - "rounded-b-none": isVertical && !isLast, - "border-t-0": isVertical && !isFirst, - }, - child.props.className, - ), - }); - })} -
- ); -}; diff --git a/src/ui/components/checkbox.tsx b/src/ui/components/checkbox.tsx index 29c5f2e..2528f41 100644 --- a/src/ui/components/checkbox.tsx +++ b/src/ui/components/checkbox.tsx @@ -12,7 +12,7 @@ function Checkbox({ + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
+ React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
+
- + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
)); CommandList.displayName = "CommandList"; const CommandGroup = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & { heading?: string } ->(({ className, heading, children, ...props }, ref) => ( -
, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + - {heading &&
{heading}
} - {children} -
+ /> )); CommandGroup.displayName = "CommandGroup"; const CommandSeparator = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
+ )); CommandSeparator.displayName = "CommandSeparator"; const CommandItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & { onSelect?: () => void } ->(({ className, onSelect, ...props }, ref) => ( -
, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
(null); + const [manuallyCollapsed, setManuallyCollapsed] = useState(false); + + useEffect(() => { + if (hasConnectionError && setIsExpanded) { + setManuallyCollapsed(false); + setIsExpanded(true); + } + }, [hasConnectionError, setIsExpanded]); + + useEffect(() => { + if (isConnected && !hasConnectionError && !isConnecting && clearLogs) { + clearLogs(); + setManuallyCollapsed(false); + } + }, [isConnected, hasConnectionError, isConnecting, clearLogs]); + + useEffect(() => { + if (lastLogRef.current) { + lastLogRef.current.scrollIntoView({ block: "end" }); + } + }, [logs]); + + const shouldShow = + !!connectionLog && + !isConnected && + (isConnecting || hasConnectionError || logs.length > 0); + + if (!shouldShow) { + return null; + } + + const expanded = isExpanded && !manuallyCollapsed; + + const handleToggle = () => { + if (hasConnectionError) { + setManuallyCollapsed((prev) => !prev); + return; + } + toggleExpanded(); + }; + + const copyLogsToClipboard = async () => { + const logsText = logs + .map((log) => { + const time = log.timestamp.toLocaleTimeString(); + return `[${time}] [${log.type.toUpperCase()}] ${log.message}`; + }) + .join("\n"); + + const ok = await copyToClipboard(logsText); + if (ok) toast.success(t("terminal.connectionLogCopied")); + else toast.error(t("terminal.connectionLogCopyFailed")); + }; + + const getIcon = (type: string) => { + switch (type) { + case "info": + return ; + case "success": + return ; + case "warning": + return ; + case "error": + return ; + default: + return ; + } + }; + + const getTextColor = (type: string) => { + switch (type) { + case "info": + return "text-blue-400"; + case "success": + return "text-green-400"; + case "warning": + return "text-yellow-400"; + case "error": + return "text-red-400"; + default: + return "text-muted-foreground"; + } + }; + + return ( +
+
+ + {logs.length > 0 && ( + + )} +
+ +
+
+ {logs.length === 0 ? ( +
+ {isConnecting + ? t("terminal.connectionLogWaiting") + : t("terminal.connectionLogEmpty")} +
+ ) : ( +
+ {logs.map((log, index) => ( +
+ + {log.timestamp.toLocaleTimeString()} + + {getIcon(log.type)} + + {log.message} + +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/ui/components/connection/ConnectionScreen.tsx b/src/ui/components/connection/ConnectionScreen.tsx new file mode 100644 index 0000000..7d259d4 --- /dev/null +++ b/src/ui/components/connection/ConnectionScreen.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils.ts"; +import { Button } from "@/components/button.tsx"; +import { RefreshCw } from "lucide-react"; +import { ConnectionLogPanel } from "@/components/connection/ConnectionLogPanel.tsx"; +import type { ConnectionStatus } from "@/components/connection/connection-status.ts"; + +interface ConnectionScreenProps { + status: ConnectionStatus; + message?: string; + backgroundColor?: string; + attempt?: number; + maxAttempts?: number; + nextRetryInMs?: number | null; + onManualRetry?: () => void; + retryLabel?: string; + disconnectedMessage?: string; + extraActions?: React.ReactNode; + logPosition?: "top" | "bottom"; + emptyState?: React.ReactNode; + className?: string; +} + +export function ConnectionScreen({ + status, + message, + backgroundColor, + attempt = 0, + maxAttempts = 0, + nextRetryInMs = null, + onManualRetry, + retryLabel, + disconnectedMessage, + extraActions, + logPosition = "bottom", + emptyState, + className, +}: ConnectionScreenProps) { + const { t } = useTranslation(); + + if (status === "connected" && !emptyState) { + return null; + } + + const showSpinner = status === "connecting"; + const showRetryButton = status === "disconnected" && !!onManualRetry; + const showLog = status !== "connected"; + + return ( +
+
+ {emptyState ? ( + emptyState + ) : ( +
+ {showSpinner &&
} + {message && ( +

+ {message} +

+ )} + {attempt > 0 && status !== "disconnected" && ( +

+ {nextRetryInMs && nextRetryInMs > 0 + ? t("connection.retryingIn", { + seconds: Math.ceil(nextRetryInMs / 1000), + attempt, + max: maxAttempts, + }) + : t("connection.retryingNow", { attempt, max: maxAttempts })} +

+ )} + {showRetryButton && ( +
+

+ {disconnectedMessage || t("connection.disconnected")} +

+
+ + {extraActions} +
+
+ )} +
+ )} +
+ + {showLog && !emptyState && ( + + )} + + +
+ ); +} diff --git a/src/ui/components/connection/connection-status.ts b/src/ui/components/connection/connection-status.ts new file mode 100644 index 0000000..53a855a --- /dev/null +++ b/src/ui/components/connection/connection-status.ts @@ -0,0 +1,40 @@ +import type { ConnectionStage } from "@/types/connection-log.ts"; + +export type ConnectionStatus = + "connecting" | "connected" | "error" | "disconnected"; + +// Guacamole client states, per guacamole-common-js Guacamole.Client#STATE_*. +const GUAC_STATE_IDLE = 0; +const GUAC_STATE_CONNECTING = 1; +const GUAC_STATE_WAITING = 2; +const GUAC_STATE_CONNECTED = 3; +const GUAC_STATE_DISCONNECTING = 4; +const GUAC_STATE_DISCONNECTED = 5; + +export function guacStateToStage(state: number): ConnectionStage { + switch (state) { + case GUAC_STATE_CONNECTING: + return "guac_connecting"; + case GUAC_STATE_WAITING: + return "guac_handshake"; + case GUAC_STATE_CONNECTED: + return "guac_ready"; + case GUAC_STATE_DISCONNECTING: + case GUAC_STATE_DISCONNECTED: + return "guac_disconnected"; + case GUAC_STATE_IDLE: + default: + return "guac_connecting"; + } +} + +export function guacStateToStatus(state: number): ConnectionStatus { + switch (state) { + case GUAC_STATE_CONNECTED: + return "connected"; + case GUAC_STATE_DISCONNECTED: + return "error"; + default: + return "connecting"; + } +} diff --git a/src/ui/components/dropdown-menu.tsx b/src/ui/components/dropdown-menu.tsx index c03b798..985a517 100644 --- a/src/ui/components/dropdown-menu.tsx +++ b/src/ui/components/dropdown-menu.tsx @@ -238,11 +238,15 @@ function DropdownMenuSubTrigger({ function DropdownMenuSubContent({ className, + sideOffset = 4, + alignOffset = -4, ...props }: React.ComponentProps) { return ( + {Icon && } + {title} + {guided && hint && ( + + {hint} + + )} + {guided && action} +
+ ); +} diff --git a/src/ui/components/popover.tsx b/src/ui/components/popover.tsx index ef5bfd0..b778bda 100644 --- a/src/ui/components/popover.tsx +++ b/src/ui/components/popover.tsx @@ -28,7 +28,7 @@ function PopoverContent({ align={align} sideOffset={sideOffset} className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", + "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-none border border-border p-4 shadow-md outline-hidden", className, )} {...props} diff --git a/src/ui/components/proxmox/ProxmoxDiscoverDialog.tsx b/src/ui/components/proxmox/ProxmoxDiscoverDialog.tsx index 7259504..fe1c0aa 100644 --- a/src/ui/components/proxmox/ProxmoxDiscoverDialog.tsx +++ b/src/ui/components/proxmox/ProxmoxDiscoverDialog.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; +import type { HostData } from "@/types/index"; import { useTranslation } from "react-i18next"; import { Server, RefreshCw, CheckSquare, Square, Download } from "lucide-react"; import { toast } from "sonner"; @@ -18,7 +19,7 @@ import { SelectValue, } from "@/components/select"; import { - discoverProxmoxGuests, + discoverProxmoxGuestsStream, bulkImportSSHHosts, getSSHHosts, } from "@/main-axios"; @@ -56,10 +57,18 @@ export function ProxmoxDiscoverDialog({ preselectedHostId ? String(preselectedHostId) : "", ); const [discovering, setDiscovering] = useState(false); + const [progress, setProgress] = useState<{ + done: number; + total: number; + } | null>(null); + const streamCloseRef = useRef<(() => void) | null>(null); const [guests, setGuests] = useState(null); const [discoveredCredentialId, setDiscoveredCredentialId] = useState< number | null >(null); + const [discoveredJumpHosts, setDiscoveredJumpHosts] = useState< + unknown[] | null + >(null); const [selected, setSelected] = useState>(new Set()); const [importing, setImporting] = useState(false); @@ -82,35 +91,50 @@ export function ProxmoxDiscoverDialog({ if (!preselectedHostId) setSelectedHostId(""); setGuests(null); setDiscoveredCredentialId(null); + setDiscoveredJumpHosts(null); setSelected(new Set()); + streamCloseRef.current?.(); + streamCloseRef.current = null; + setProgress(null); setDiscovering(false); setImporting(false); } - async function handleDiscover() { + function handleDiscover() { const hostId = preselectedHostId ?? (selectedHostId ? Number(selectedHostId) : null); if (!hostId) return; setDiscovering(true); setGuests(null); setDiscoveredCredentialId(null); + setDiscoveredJumpHosts(null); setSelected(new Set()); - try { - const result = await discoverProxmoxGuests(hostId); - setGuests(result.guests); - setDiscoveredCredentialId(result.credentialId ?? null); - setSelected( - new Set( - result.guests - .filter((g) => g.status === "running") - .map((g) => g.vmid), - ), - ); - } catch (err: any) { - toast.error(err?.message ?? t("hosts.proxmoxDiscoveryFailed")); - } finally { - setDiscovering(false); - } + setProgress(null); + streamCloseRef.current?.(); + streamCloseRef.current = discoverProxmoxGuestsStream(hostId, { + onProgress: (done, total) => setProgress({ done, total }), + onResult: (result) => { + setGuests(result.guests); + setDiscoveredCredentialId(result.credentialId ?? null); + setDiscoveredJumpHosts(result.jumpHosts ?? null); + setSelected( + new Set( + result.guests + .filter((g) => g.status === "running") + .map((g) => g.vmid), + ), + ); + setDiscovering(false); + setProgress(null); + streamCloseRef.current = null; + }, + onError: (message) => { + toast.error(message ?? t("hosts.proxmoxDiscoveryFailed")); + setDiscovering(false); + setProgress(null); + streamCloseRef.current = null; + }, + }); } async function handleImport() { @@ -121,41 +145,58 @@ export function ProxmoxDiscoverDialog({ const credId = defaultCredentialId ?? discoveredCredentialId; const importAuth = resolveProxmoxImportAuth(defaultAuthType, credId); - const toImport = guests - .filter((g) => selected.has(g.vmid)) - .map((g) => ({ - name: g.name, - ip: g.ip ?? "0.0.0.0", - port: g.connectionType === "rdp" ? 3389 : 22, - username: defaultUsername ?? "root", - folder: importFolder, - ...importAuth, - enableTerminal: g.connectionType !== "rdp", - enableFileManager: g.connectionType !== "rdp", - enableTunnel: g.connectionType !== "rdp", - enableSsh: g.connectionType !== "rdp", - enableRdp: g.connectionType === "rdp", - enableDocker: g.enableDocker, - connectionType: g.connectionType, - tags: ["proxmox", g.type, g.node], - proxmoxConfig: { - source: { - source: "proxmox", - sourceHostId: Number(effectiveHostId), - node: g.node, - vmid: g.vmid, - type: g.type, - lastSeenAt: new Date().toISOString(), - lastStatus: g.status, - missingSince: null, - }, - }, - })); + const selectedGuests = guests.filter((g) => selected.has(g.vmid)); - const result = await bulkImportSSHHosts(toImport, false); - const updated = await getSSHHosts(); - onHostsChanged(updated); - window.dispatchEvent(new CustomEvent("termix:hosts-changed")); + const toImport = selectedGuests.map((g) => ({ + name: g.name, + // No IP discovered (e.g. QEMU without a running guest agent): import + // with a placeholder so the host is created and the user can fill in + // the real IP. Re-sync keeps the manual value (guest.ip || existing.ip). + ip: g.ip || "0.0.0.0", + port: g.connectionType === "rdp" ? 3389 : 22, + username: defaultUsername ?? "root", + folder: importFolder, + // Inherit the jump-host chain from the scanned Proxmox host so the + // imported guests are reachable the same way; user can override. + jumpHosts: discoveredJumpHosts ?? undefined, + ...importAuth, + enableTerminal: g.connectionType !== "rdp", + enableFileManager: g.connectionType !== "rdp", + enableTunnel: g.connectionType !== "rdp", + enableSsh: g.connectionType !== "rdp", + enableRdp: g.connectionType === "rdp", + enableDocker: g.enableDocker, + connectionType: g.connectionType, + tags: [ + "proxmox", + g.type, + g.node, + g.type === "lxc" ? `ct-${g.vmid}` : `vm-${g.vmid}`, + ...(g.enableDocker ? ["docker"] : []), + ], + proxmoxConfig: { + source: { + source: "proxmox", + sourceHostId: Number(effectiveHostId), + node: g.node, + vmid: g.vmid, + type: g.type, + lastSeenAt: new Date().toISOString(), + lastStatus: g.status, + missingSince: null, + }, + }, + })); + + const result = toImport.length + ? await bulkImportSSHHosts(toImport as unknown as HostData[], false) + : { success: 0, updated: 0, skipped: 0, failed: 0 }; + + if (toImport.length) { + const updated = await getSSHHosts(); + onHostsChanged(updated); + window.dispatchEvent(new CustomEvent("termix:hosts-changed")); + } const msg = [ result.success @@ -274,7 +315,9 @@ export function ProxmoxDiscoverDialog({ className={`size-3.5 mr-1.5 ${discovering ? "animate-spin" : ""}`} /> {discovering - ? t("hosts.proxmoxDiscovering") + ? progress + ? `${t("hosts.proxmoxDiscovering")} ${progress.done}/${progress.total}` + : t("hosts.proxmoxDiscovering") : t("hosts.proxmoxDiscoverGuests")} )} @@ -346,11 +389,15 @@ export function ProxmoxDiscoverDialog({ > {g.status} - {g.ip && ( - - {g.ip} - - )} + + {g.ip || "no IP"} + ))}
diff --git a/src/ui/components/proxmox/proxmox-import-auth.ts b/src/ui/components/proxmox/proxmox-import-auth.ts index b4257b0..e96dbf2 100644 --- a/src/ui/components/proxmox/proxmox-import-auth.ts +++ b/src/ui/components/proxmox/proxmox-import-auth.ts @@ -1,5 +1,11 @@ const SECRET_BACKED_AUTH_TYPES = new Set(["password", "key"]); -const SECRETLESS_AUTH_TYPES = new Set(["none", "opkssh", "tailscale", "vault"]); +const SECRETLESS_AUTH_TYPES = new Set([ + "none", + "agent", + "opkssh", + "tailscale", + "vault", +]); export type ProxmoxImportAuth = { authType: string; @@ -11,21 +17,29 @@ export function resolveProxmoxImportAuth( defaultAuthType: string | undefined, credentialId: number | null | undefined, ): ProxmoxImportAuth { - if (defaultAuthType === "credential" || (!defaultAuthType && credentialId)) { - return credentialId - ? { - authType: "credential", - credentialId, - overrideCredentialUsername: true, - } - : { authType: "none" }; - } - + // An explicit secretless auth choice (none/opkssh/tailscale/vault) wins. if (defaultAuthType && SECRETLESS_AUTH_TYPES.has(defaultAuthType)) { return { authType: defaultAuthType }; } - if (defaultAuthType && !SECRET_BACKED_AUTH_TYPES.has(defaultAuthType)) { + // A credential (configured default OR inherited from the source Proxmox host) + // is a concrete auth source -> use it, even when defaultAuthType is the + // "password"/"key" default. Otherwise imported guests end up as authType + // "none" although the host authenticates via a credential. + if (credentialId) { + return { + authType: "credential", + credentialId, + overrideCredentialUsername: true, + }; + } + + // Explicit non secret-backed special type without a credential. + if ( + defaultAuthType && + defaultAuthType !== "credential" && + !SECRET_BACKED_AUTH_TYPES.has(defaultAuthType) + ) { return { authType: defaultAuthType }; } diff --git a/src/ui/components/section-card.tsx b/src/ui/components/section-card.tsx index b0e0d1e..f20485b 100644 --- a/src/ui/components/section-card.tsx +++ b/src/ui/components/section-card.tsx @@ -6,14 +6,18 @@ export function SectionCard({ icon, action, children, + className, }: { title: string; icon: React.ReactNode; action?: React.ReactNode; children: React.ReactNode; + className?: string; }) { return ( -
+
{icon} diff --git a/src/ui/components/select.tsx b/src/ui/components/select.tsx index dadd352..9207fe8 100644 --- a/src/ui/components/select.tsx +++ b/src/ui/components/select.tsx @@ -35,7 +35,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-none border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} @@ -59,7 +59,7 @@ function SelectContent({ & { - status: "online" | "offline" | "maintenance" | "degraded"; -}; - -export const Status = ({ className, status, ...props }: StatusProps) => ( - -); - -export type StatusIndicatorProps = HTMLAttributes; - -export const StatusIndicator = ({ ...props }: StatusIndicatorProps) => ( - - - - -); - -export type StatusLabelProps = HTMLAttributes; - -export const StatusLabel = ({ - className, - children, - ...props -}: StatusLabelProps) => { - const { t } = useTranslation(); - return ( - - {children ?? ( - <> - - {t("common.online")} - - - {t("common.offline")} - - - {t("common.maintenance")} - - - {t("common.degraded")} - - - )} - - ); -}; diff --git a/src/ui/components/tabs.tsx b/src/ui/components/tabs.tsx index 8e30362..b3a3d0b 100644 --- a/src/ui/components/tabs.tsx +++ b/src/ui/components/tabs.tsx @@ -24,7 +24,7 @@ function TabsList({ ( return (