Merge branch 'dev-1.10.1' into feat/url-terminal-route

This commit is contained in:
Luke Gustafson
2026-01-12 23:56:56 -06:00
committed by GitHub
50 changed files with 4751 additions and 63859 deletions

32
.github/workflows/openapi.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Generate OpenAPI Specification
on:
workflow_dispatch:
jobs:
generate-openapi:
name: Generate OpenAPI JSON
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate OpenAPI specification
run: npm run generate:openapi
- name: Upload OpenAPI artifact
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: openapi.json
retention-days: 90

View File

@@ -1,437 +0,0 @@
name: Auto Translate
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
translate-zh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t zh --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-zh
path: src/locales/zh.json
continue-on-error: true
translate-ru:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ru --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-ru
path: src/locales/ru.json
continue-on-error: true
translate-pt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t pt --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-pt
path: src/locales/pt.json
continue-on-error: true
translate-fr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t fr --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-fr
path: src/locales/fr.json
continue-on-error: true
translate-es:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t es --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-es
path: src/locales/es.json
continue-on-error: true
translate-de:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t de --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-de
path: src/locales/de.json
continue-on-error: true
translate-hi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t hi --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-hi
path: src/locales/hi.json
continue-on-error: true
translate-bn:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t bn --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-bn
path: src/locales/bn.json
continue-on-error: true
translate-ja:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ja --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-ja
path: src/locales/ja.json
continue-on-error: true
translate-vi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t vi --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-vi
path: src/locales/vi.json
continue-on-error: true
translate-tr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t tr --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-tr
path: src/locales/tr.json
continue-on-error: true
translate-ko:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ko --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-ko
path: src/locales/ko.json
continue-on-error: true
translate-it:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t it --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-it
path: src/locales/it.json
continue-on-error: true
translate-he:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t he --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-he
path: src/locales/he.json
continue-on-error: true
translate-ar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ar --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-ar
path: src/locales/ar.json
continue-on-error: true
translate-pl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t pl --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-pl
path: src/locales/pl.json
continue-on-error: true
translate-nl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t nl --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-nl
path: src/locales/nl.json
continue-on-error: true
translate-sv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t sv --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-sv
path: src/locales/sv.json
continue-on-error: true
translate-id:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t id --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-id
path: src/locales/id.json
continue-on-error: true
translate-th:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t th --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-th
path: src/locales/th.json
continue-on-error: true
translate-uk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t uk --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-uk
path: src/locales/uk.json
continue-on-error: true
translate-cs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t cs --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-cs
path: src/locales/cs.json
continue-on-error: true
translate-ro:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ro --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-ro
path: src/locales/ro.json
continue-on-error: true
translate-el:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t el --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-el
path: src/locales/el.json
continue-on-error: true
translate-nb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t nb --maxLinesPerRequest 1
- uses: actions/upload-artifact@v4
with:
name: translations-nb
path: src/locales/nb.json
continue-on-error: true
create-pr:
needs:
[
translate-zh,
translate-ru,
translate-pt,
translate-fr,
translate-es,
translate-de,
translate-hi,
translate-bn,
translate-ja,
translate-vi,
translate-tr,
translate-ko,
translate-it,
translate-he,
translate-ar,
translate-pl,
translate-nl,
translate-sv,
translate-id,
translate-th,
translate-uk,
translate-cs,
translate-ro,
translate-el,
translate-nb,
]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GHCR_TOKEN }}
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: translations-temp
- name: Move translations to src/locales
run: |
cp translations-temp/translations-zh/zh.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-ru/ru.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-pt/pt.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-fr/fr.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-es/es.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-de/de.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-hi/hi.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-bn/bn.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-ja/ja.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-vi/vi.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-tr/tr.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-ko/ko.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-it/it.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-he/he.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-ar/ar.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-pl/pl.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-nl/nl.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-sv/sv.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-id/id.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-th/th.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-uk/uk.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-cs/cs.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-ro/ro.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-el/el.json src/locales/ 2>/dev/null || true
cp translations-temp/translations-nb/nb.json src/locales/ 2>/dev/null || true
rm -rf translations-temp
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GHCR_TOKEN }}
commit-message: "chore: auto-translate to multiple languages"
branch: translations-update
delete-branch: true
title: "chore: Update translations for all languages"

File diff suppressed because it is too large Load Diff

262
package-lock.json generated
View File

@@ -34,6 +34,7 @@
"@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.9",
"@types/cytoscape": "^3.21.9",
"@types/jszip": "^3.4.0",
"@types/multer": "^2.0.0",
"@types/qrcode": "^1.5.5",
@@ -56,6 +57,7 @@
"cmdk": "^1.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"cytoscape": "^3.33.1",
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
@@ -72,6 +74,7 @@
"node-fetch": "^3.3.2",
"qrcode": "^1.5.4",
"react": "^19.1.0",
"react-cytoscapejs": "^2.0.0",
"react-dom": "^19.1.0",
"react-h5-audio-player": "^3.10.1",
"react-hook-form": "^7.60.0",
@@ -122,11 +125,60 @@
"husky": "^9.1.7",
"lint-staged": "^16.2.3",
"prettier": "3.6.2",
"swagger-jsdoc": "^6.2.8",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0",
"vite": "^7.1.5"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"dev": true,
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -2619,6 +2671,13 @@
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true,
"license": "MIT"
},
"node_modules/@lezer/common": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
@@ -5228,6 +5287,12 @@
"@types/node": "*"
}
},
"node_modules/@types/cytoscape": {
"version": "3.21.9",
"resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz",
"integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==",
"license": "MIT"
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -6958,6 +7023,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -7776,6 +7848,15 @@
"integrity": "sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==",
"license": "MIT"
},
"node_modules/cytoscape": {
"version": "3.33.1",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"engines": {
"node": ">=0.10"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@@ -8304,6 +8385,19 @@
"license": "MIT",
"optional": true
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/dompurify": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
@@ -12195,6 +12289,14 @@
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -12207,6 +12309,14 @@
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -14068,6 +14178,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -15109,6 +15227,19 @@
"node": ">=0.10.0"
}
},
"node_modules/react-cytoscapejs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-cytoscapejs/-/react-cytoscapejs-2.0.0.tgz",
"integrity": "sha512-t3SSl1DQy7+JQjN+8QHi1anEJlM3i3aAeydHTsJwmjo/isyKK7Rs7oCvU6kZsB9NwZidzZQR21Vm2PcBLG/Tjg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"cytoscape": "^3.2.19",
"react": ">=15.0.0"
}
},
"node_modules/react-dom": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
@@ -16717,6 +16848,95 @@
"node": ">=8"
}
},
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/swagger-jsdoc/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-jsdoc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/swagger-jsdoc/node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tailwind-merge": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
@@ -17566,6 +17786,16 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/validator": {
"version": "13.15.26",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -18120,6 +18350,38 @@
"integrity": "sha512-YHDIOAqgRpfl1Ois9HcB8UFtWOxK8KJrV5TXpImj4BKYP1rWT04f/fMM9tQ9SYZlBKukT7NR+9wcI3UpB5BMDQ==",
"license": "MIT"
},
"node_modules/z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=8.0.0"
},
"optionalDependencies": {
"commander": "^9.4.1"
}
},
"node_modules/z-schema/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/zod": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",

View File

@@ -17,6 +17,7 @@
"build": "vite build && tsc -p tsconfig.node.json",
"build:backend": "tsc -p tsconfig.node.json",
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
"generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js",
"preview": "vite preview",
"electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"",
"build:win-portable": "npm run build && electron-builder --win --dir",
@@ -144,6 +145,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.2.3",
"prettier": "3.6.2",
"swagger-jsdoc": "^6.2.8",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0",
"vite": "^7.1.5"

View File

@@ -58,6 +58,31 @@ app.use(express.json({ limit: "1mb" }));
app.use(authManager.createAuthMiddleware());
/**
* @openapi
* /uptime:
* get:
* summary: Get server uptime
* description: Returns the uptime of the server in various formats.
* tags:
* - Dashboard
* responses:
* 200:
* description: Server uptime information.
* content:
* application/json:
* schema:
* type: object
* properties:
* uptimeMs:
* type: number
* uptimeSeconds:
* type: number
* formatted:
* type: string
* 500:
* description: Failed to get uptime.
*/
app.get("/uptime", async (req, res) => {
try {
const uptimeMs = Date.now() - serverStartTime;
@@ -77,6 +102,28 @@ app.get("/uptime", async (req, res) => {
}
});
/**
* @openapi
* /activity/recent:
* get:
* summary: Get recent activity
* description: Fetches the most recent activities for the authenticated user.
* tags:
* - Dashboard
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* description: The maximum number of activities to return.
* responses:
* 200:
* description: A list of recent activities.
* 401:
* description: Session expired.
* 500:
* description: Failed to get recent activity.
*/
app.get("/activity/recent", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -108,6 +155,40 @@ app.get("/activity/recent", async (req, res) => {
}
});
/**
* @openapi
* /activity/log:
* post:
* summary: Log a new activity
* description: Logs a new user activity, such as accessing a terminal or file manager. This endpoint is rate-limited.
* tags:
* - Dashboard
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* type:
* type: string
* enum: [terminal, file_manager, server_stats, tunnel, docker]
* hostId:
* type: integer
* hostName:
* type: string
* responses:
* 200:
* description: Activity logged successfully or rate-limited.
* 400:
* description: Invalid request body.
* 401:
* description: Session expired.
* 404:
* description: Host not found or access denied.
* 500:
* description: Failed to log activity.
*/
app.post("/activity/log", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -224,6 +305,22 @@ app.post("/activity/log", async (req, res) => {
}
});
/**
* @openapi
* /activity/reset:
* delete:
* summary: Reset recent activity
* description: Clears all recent activity for the authenticated user.
* tags:
* - Dashboard
* responses:
* 200:
* description: Recent activity cleared.
* 401:
* description: Session expired.
* 500:
* description: Failed to reset activity.
*/
app.delete("/activity/reset", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;

View File

@@ -206,10 +206,46 @@ app.use(bodyParser.urlencoded({ limit: "1gb", extended: true }));
app.use(bodyParser.raw({ limit: "5gb", type: "application/octet-stream" }));
app.use(cookieParser());
/**
* @openapi
* /health:
* get:
* summary: Health check
* description: Returns the health status of the server.
* tags:
* - General
* responses:
* 200:
* description: Server is healthy.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: ok
*/
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
/**
* @openapi
* /version:
* get:
* summary: Get version information
* description: Returns the local and remote version of the application.
* tags:
* - General
* responses:
* 200:
* description: Version information.
* 404:
* description: Local version not set.
* 500:
* description: Fetch error.
*/
app.get("/version", authenticateJWT, async (req, res) => {
let localVersion = process.env.VERSION;
@@ -308,6 +344,31 @@ app.get("/version", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /releases/rss:
* get:
* summary: Get releases in RSS format
* description: Returns the latest releases from the GitHub repository in an RSS-like JSON format.
* tags:
* - General
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: The page number of the releases to fetch.
* - in: query
* name: per_page
* schema:
* type: integer
* description: The number of releases to fetch per page.
* responses:
* 200:
* description: Releases in RSS format.
* 500:
* description: Failed to generate RSS format.
*/
app.get("/releases/rss", authenticateJWT, async (req, res) => {
try {
const page = parseInt(req.query.page as string) || 1;
@@ -364,6 +425,20 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /encryption/status:
* get:
* summary: Get encryption status
* description: Returns the security status of the application.
* tags:
* - Encryption
* responses:
* 200:
* description: Security status.
* 500:
* description: Failed to get security status.
*/
app.get("/encryption/status", requireAdmin, async (req, res) => {
try {
const securityStatus = {
@@ -385,6 +460,20 @@ app.get("/encryption/status", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /encryption/initialize:
* post:
* summary: Initialize security system
* description: Initializes the security system for the application.
* tags:
* - Encryption
* responses:
* 200:
* description: Security system initialized successfully.
* 500:
* description: Failed to initialize security system.
*/
app.post("/encryption/initialize", requireAdmin, async (req, res) => {
try {
const authManager = AuthManager.getInstance();
@@ -408,6 +497,20 @@ app.post("/encryption/initialize", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /encryption/regenerate:
* post:
* summary: Regenerate JWT secret
* description: Regenerates the system JWT secret. This will invalidate all existing JWT tokens.
* tags:
* - Encryption
* responses:
* 200:
* description: System JWT secret regenerated.
* 500:
* description: Failed to regenerate JWT secret.
*/
app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
try {
apiLogger.warn("System JWT secret regenerated via API", {
@@ -429,6 +532,20 @@ app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /encryption/regenerate-jwt:
* post:
* summary: Regenerate JWT secret
* description: Regenerates the JWT secret. This will invalidate all existing JWT tokens.
* tags:
* - Encryption
* responses:
* 200:
* description: New JWT secret generated.
* 500:
* description: Failed to regenerate JWT secret.
*/
app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
try {
apiLogger.warn("JWT secret regenerated via API", {
@@ -449,6 +566,33 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /database/export:
* post:
* summary: Export user data
* description: Exports the user's data as a SQLite database file.
* tags:
* - Database
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* password:
* type: string
* responses:
* 200:
* description: User data exported successfully.
* 400:
* description: Password required for export.
* 401:
* description: Invalid password.
* 500:
* description: Failed to export user data.
*/
app.post("/database/export", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -899,6 +1043,36 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /database/import:
* post:
* summary: Import user data
* description: Imports user data from a SQLite database file.
* tags:
* - Database
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* file:
* type: string
* format: binary
* password:
* type: string
* responses:
* 200:
* description: Incremental import completed successfully.
* 400:
* description: No file uploaded or password required for import.
* 401:
* description: Invalid password.
* 500:
* description: Failed to import SQLite data.
*/
app.post(
"/database/import",
authenticateJWT,
@@ -1363,6 +1537,31 @@ app.post(
},
);
/**
* @openapi
* /database/export/preview:
* post:
* summary: Preview user data export
* description: Generates a preview of the user data export, including statistics about the data.
* tags:
* - Database
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* scope:
* type: string
* includeCredentials:
* type: boolean
* responses:
* 200:
* description: Export preview generated successfully.
* 500:
* description: Failed to generate export preview.
*/
app.post("/database/export/preview", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -1398,6 +1597,33 @@ app.post("/database/export/preview", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /database/restore:
* post:
* summary: Restore database from backup
* description: Restores the database from an encrypted backup file.
* tags:
* - Database
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* backupPath:
* type: string
* targetPath:
* type: string
* responses:
* 200:
* description: Database restored successfully.
* 400:
* description: Backup path is required or invalid encrypted backup file.
* 500:
* description: Database restore failed.
*/
app.post("/database/restore", requireAdmin, async (req, res) => {
try {
const { backupPath, targetPath } = req.body;
@@ -1479,6 +1705,20 @@ async function initializeSecurity() {
}
}
/**
* @openapi
* /database/migration/status:
* get:
* summary: Get database migration status
* description: Returns the status of the database migration.
* tags:
* - Database
* responses:
* 200:
* description: Migration status.
* 500:
* description: Failed to get migration status.
*/
app.get(
"/database/migration/status",
authenticateJWT,
@@ -1532,6 +1772,20 @@ app.get(
},
);
/**
* @openapi
* /database/migration/history:
* get:
* summary: Get database migration history
* description: Returns the history of database migrations.
* tags:
* - Database
* responses:
* 200:
* description: Migration history.
* 500:
* description: Failed to get migration history.
*/
app.get(
"/database/migration/history",
authenticateJWT,

View File

@@ -671,7 +671,8 @@ const migrateSchema = () => {
`);
} catch (createError) {
databaseLogger.warn("Failed to create network_topology table", {
error: createError instanceof Error ? createError.message : String(createError),
operation: "schema_migration",
error: createError,
});
}
}

View File

@@ -99,8 +99,20 @@ const router = express.Router();
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
// Route: Get alerts for the authenticated user (excluding dismissed ones)
// GET /alerts
/**
* @openapi
* /alerts:
* get:
* summary: Get active alerts
* description: Fetches active alerts for the authenticated user, excluding those that have been dismissed.
* tags:
* - Alerts
* responses:
* 200:
* description: A list of active alerts.
* 500:
* description: Failed to fetch alerts.
*/
router.get("/", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -131,8 +143,33 @@ router.get("/", authenticateJWT, async (req, res) => {
}
});
// Route: Dismiss an alert for the authenticated user
// POST /alerts/dismiss
/**
* @openapi
* /alerts/dismiss:
* post:
* summary: Dismiss an alert
* description: Marks an alert as dismissed for the authenticated user.
* tags:
* - Alerts
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* alertId:
* type: string
* responses:
* 200:
* description: Alert dismissed successfully.
* 400:
* description: Alert ID is required.
* 409:
* description: Alert already dismissed.
* 500:
* description: Failed to dismiss alert.
*/
router.post("/dismiss", authenticateJWT, async (req, res) => {
try {
const { alertId } = req.body;
@@ -170,8 +207,20 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
}
});
// Route: Get dismissed alerts for a user
// GET /alerts/dismissed/:userId
/**
* @openapi
* /alerts/dismissed:
* get:
* summary: Get dismissed alerts
* description: Fetches a list of alerts that have been dismissed by the authenticated user.
* tags:
* - Alerts
* responses:
* 200:
* description: A list of dismissed alerts.
* 500:
* description: Failed to fetch dismissed alerts.
*/
router.get("/dismissed", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -194,8 +243,33 @@ router.get("/dismissed", authenticateJWT, async (req, res) => {
}
});
// Route: Undismiss an alert for the authenticated user (remove from dismissed list)
// DELETE /alerts/dismiss
/**
* @openapi
* /alerts/dismiss:
* delete:
* summary: Undismiss an alert
* description: Removes an alert from the dismissed list for the authenticated user.
* tags:
* - Alerts
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* alertId:
* type: string
* responses:
* 200:
* description: Alert undismissed successfully.
* 400:
* description: Alert ID is required.
* 404:
* description: Dismissed alert not found.
* 500:
* description: Failed to undismiss alert.
*/
router.delete("/dismiss", authenticateJWT, async (req, res) => {
try {
const { alertId } = req.body;

View File

@@ -84,8 +84,52 @@ const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Create a new credential
// POST /credentials
/**
* @openapi
* /credentials:
* post:
* summary: Create a new credential
* description: Creates a new SSH credential for the authenticated user.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* folder:
* type: string
* tags:
* type: array
* items:
* type: string
* authType:
* type: string
* enum: [password, key]
* username:
* type: string
* password:
* type: string
* key:
* type: string
* keyPassword:
* type: string
* keyType:
* type: string
* responses:
* 201:
* description: Credential created successfully.
* 400:
* description: Invalid request body.
* 500:
* description: Failed to create credential.
*/
router.post(
"/",
authenticateJWT,
@@ -231,8 +275,22 @@ router.post(
},
);
// Get all credentials for the authenticated user
// GET /credentials
/**
* @openapi
* /credentials:
* get:
* summary: Get all credentials
* description: Retrieves all SSH credentials for the authenticated user.
* tags:
* - Credentials
* responses:
* 200:
* description: A list of credentials.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch credentials.
*/
router.get(
"/",
authenticateJWT,
@@ -264,8 +322,22 @@ router.get(
},
);
// Get all unique credential folders for the authenticated user
// GET /credentials/folders
/**
* @openapi
* /credentials/folders:
* get:
* summary: Get credential folders
* description: Retrieves all unique credential folders for the authenticated user.
* tags:
* - Credentials
* responses:
* 200:
* description: A list of folder names.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch credential folders.
*/
router.get(
"/folders",
authenticateJWT,
@@ -302,8 +374,30 @@ router.get(
},
);
// Get a specific credential by ID (with plain text secrets)
// GET /credentials/:id
/**
* @openapi
* /credentials/{id}:
* get:
* summary: Get a specific credential
* description: Retrieves a specific credential by its ID, including secrets.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The requested credential.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to fetch credential.
*/
router.get(
"/:id",
authenticateJWT,
@@ -366,8 +460,41 @@ router.get(
},
);
// Update a credential
// PUT /credentials/:id
/**
* @openapi
* /credentials/{id}:
* put:
* summary: Update a credential
* description: Updates a specific credential by its ID.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* responses:
* 200:
* description: The updated credential.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to update credential.
*/
router.put(
"/:id",
authenticateJWT,
@@ -510,8 +637,30 @@ router.put(
},
);
// Delete a credential
// DELETE /credentials/:id
/**
* @openapi
* /credentials/{id}:
* delete:
* summary: Delete a credential
* description: Deletes a specific credential by its ID.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Credential deleted successfully.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to delete credential.
*/
router.delete(
"/:id",
authenticateJWT,
@@ -626,8 +775,35 @@ router.delete(
},
);
// Apply a credential to an SSH host (for quick application)
// POST /credentials/:id/apply-to-host/:hostId
/**
* @openapi
* /credentials/{id}/apply-to-host/{hostId}:
* post:
* summary: Apply a credential to a host
* description: Applies a credential to an SSH host for quick application.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Credential applied to host successfully.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to apply credential to host.
*/
router.post(
"/:id/apply-to-host/:hostId",
authenticateJWT,
@@ -705,8 +881,28 @@ router.post(
},
);
// Get hosts using a specific credential
// GET /credentials/:id/hosts
/**
* @openapi
* /credentials/{id}/hosts:
* get:
* summary: Get hosts using a credential
* description: Retrieves a list of hosts that are using a specific credential.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of hosts.
* 400:
* description: Invalid request.
* 500:
* description: Failed to fetch hosts using credential.
*/
router.get(
"/:id/hosts",
authenticateJWT,
@@ -800,8 +996,33 @@ function formatSSHHostOutput(
};
}
// Rename a credential folder
// PUT /credentials/folders/rename
/**
* @openapi
* /credentials/folders/rename:
* put:
* summary: Rename a credential folder
* description: Renames a credential folder for the authenticated user.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* oldName:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Folder renamed successfully.
* 400:
* description: Both oldName and newName are required.
* 500:
* description: Failed to rename folder.
*/
router.put(
"/folders/rename",
authenticateJWT,
@@ -840,8 +1061,33 @@ router.put(
},
);
// Detect SSH key type endpoint
// POST /credentials/detect-key-type
/**
* @openapi
* /credentials/detect-key-type:
* post:
* summary: Detect SSH key type
* description: Detects the type of an SSH private key.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* privateKey:
* type: string
* keyPassword:
* type: string
* responses:
* 200:
* description: Key type detection result.
* 400:
* description: Private key is required.
* 500:
* description: Failed to detect key type.
*/
router.post(
"/detect-key-type",
authenticateJWT,
@@ -874,8 +1120,31 @@ router.post(
},
);
// Detect SSH public key type endpoint
// POST /credentials/detect-public-key-type
/**
* @openapi
* /credentials/detect-public-key-type:
* post:
* summary: Detect SSH public key type
* description: Detects the type of an SSH public key.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* publicKey:
* type: string
* responses:
* 200:
* description: Key type detection result.
* 400:
* description: Public key is required.
* 500:
* description: Failed to detect public key type.
*/
router.post(
"/detect-public-key-type",
authenticateJWT,
@@ -909,8 +1178,35 @@ router.post(
},
);
// Validate SSH key pair endpoint
// POST /credentials/validate-key-pair
/**
* @openapi
* /credentials/validate-key-pair:
* post:
* summary: Validate SSH key pair
* description: Validates if a given SSH private key and public key match.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* privateKey:
* type: string
* publicKey:
* type: string
* keyPassword:
* type: string
* responses:
* 200:
* description: Key pair validation result.
* 400:
* description: Private key and public key are required.
* 500:
* description: Failed to validate key pair.
*/
router.post(
"/validate-key-pair",
authenticateJWT,
@@ -953,8 +1249,32 @@ router.post(
},
);
// Generate new SSH key pair endpoint
// POST /credentials/generate-key-pair
/**
* @openapi
* /credentials/generate-key-pair:
* post:
* summary: Generate new SSH key pair
* description: Generates a new SSH key pair.
* tags:
* - Credentials
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* keyType:
* type: string
* keySize:
* type: integer
* passphrase:
* type: string
* responses:
* 200:
* description: The new key pair.
* 500:
* description: Failed to generate SSH key pair.
*/
router.post(
"/generate-key-pair",
authenticateJWT,
@@ -996,8 +1316,33 @@ router.post(
},
);
// Generate public key from private key endpoint
// POST /credentials/generate-public-key
/**
* @openapi
* /credentials/generate-public-key:
* post:
* summary: Generate public key from private key
* description: Generates a public key from a given private key.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* privateKey:
* type: string
* keyPassword:
* type: string
* responses:
* 200:
* description: The generated public key.
* 400:
* description: Private key is required.
* 500:
* description: Failed to generate public key.
*/
router.post(
"/generate-public-key",
authenticateJWT,
@@ -1283,7 +1628,7 @@ async function deploySSHKeyToHost(
.replace(/'/g, "'\\''");
conn.exec(
`printf '%s\\n' '${escapedKey} ${credData.name}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
`printf '%s\n' '${escapedKey} ${credData.name}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
(err, stream) => {
if (err) {
clearTimeout(addTimeout);
@@ -1502,8 +1847,41 @@ async function deploySSHKeyToHost(
});
}
// Deploy SSH Key to Host endpoint
// POST /credentials/:id/deploy-to-host
/**
* @openapi
* /credentials/{id}/deploy-to-host:
* post:
* summary: Deploy SSH key to a host
* description: Deploys an SSH public key to a target host's authorized_keys file.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* targetHostId:
* type: integer
* responses:
* 200:
* description: SSH key deployed successfully.
* 400:
* description: Credential ID and target host ID are required.
* 401:
* description: Authentication required.
* 404:
* description: Credential or target host not found.
* 500:
* description: Failed to deploy SSH key.
*/
router.post(
"/:id/deploy-to-host",
authenticateJWT,

View File

@@ -9,6 +9,22 @@ const router = express.Router();
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
/**
* @openapi
* /network-topology:
* get:
* summary: Get network topology
* description: Retrieves the network topology for the authenticated user.
* tags:
* - Network Topology
* responses:
* 200:
* description: The network topology.
* 401:
* description: User not authenticated.
* 500:
* description: Failed to fetch network topology.
*/
router.get(
"/",
authenticateJWT,
@@ -24,7 +40,7 @@ router.get(
.select()
.from(networkTopology)
.where(eq(networkTopology.userId, userId));
if (result.length > 0) {
const topologyStr = result[0].topology;
const topology = topologyStr ? JSON.parse(topologyStr) : null;
@@ -34,11 +50,43 @@ router.get(
}
} catch (error) {
console.error("Error fetching network topology:", error);
return res.status(500).json({ error: "Failed to fetch network topology", details: (error as Error).message });
return res
.status(500)
.json({
error: "Failed to fetch network topology",
details: (error as Error).message,
});
}
},
);
/**
* @openapi
* /network-topology:
* post:
* summary: Save network topology
* description: Saves the network topology for the authenticated user.
* tags:
* - Network Topology
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* topology:
* type: object
* responses:
* 200:
* description: Network topology saved successfully.
* 400:
* description: Topology data is required.
* 401:
* description: User not authenticated.
* 500:
* description: Failed to save network topology.
*/
router.post(
"/",
authenticateJWT,
@@ -55,15 +103,16 @@ router.post(
}
const db = getDb();
// Ensure topology is a string
const topologyStr = typeof topology === 'string' ? topology : JSON.stringify(topology);
const topologyStr =
typeof topology === "string" ? topology : JSON.stringify(topology);
const existing = await db
.select()
.from(networkTopology)
.where(eq(networkTopology.userId, userId));
if (existing.length > 0) {
// Update existing record
await db
@@ -76,11 +125,16 @@ router.post(
.insert(networkTopology)
.values({ userId, topology: topologyStr });
}
return res.json({ success: true });
} catch (error) {
console.error("Error saving network topology:", error);
return res.status(500).json({ error: "Failed to save network topology", details: (error as Error).message });
return res
.status(500)
.json({
error: "Failed to save network topology",
details: (error as Error).message,
});
}
},
);

View File

@@ -27,8 +27,51 @@ function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
//Share a host with a user or role
//POST /rbac/host/:id/share
/**
* @openapi
* /rbac/host/{id}/share:
* post:
* summary: Share a host
* description: Shares a host with a user or a role.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* targetType:
* type: string
* enum: [user, role]
* targetUserId:
* type: string
* targetRoleId:
* type: integer
* durationHours:
* type: number
* permissionLevel:
* type: string
* enum: [view]
* responses:
* 200:
* description: Host shared successfully.
* 400:
* description: Invalid request body.
* 403:
* description: Not host owner.
* 404:
* description: Target user or role not found.
* 500:
* description: Failed to share host.
*/
router.post(
"/host/:id/share",
authenticateJWT,
@@ -227,8 +270,35 @@ router.post(
},
);
// Revoke host access
// DELETE /rbac/host/:id/access/:accessId
/**
* @openapi
* /rbac/host/{id}/access/{accessId}:
* delete:
* summary: Revoke host access
* description: Revokes a user's or role's access to a host.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* - in: path
* name: accessId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Access revoked successfully.
* 400:
* description: Invalid ID.
* 403:
* description: Not host owner.
* 500:
* description: Failed to revoke access.
*/
router.delete(
"/host/:id/access/:accessId",
authenticateJWT,
@@ -267,8 +337,30 @@ router.delete(
},
);
// Get host access list
// GET /rbac/host/:id/access
/**
* @openapi
* /rbac/host/{id}/access:
* get:
* summary: Get host access list
* description: Retrieves the list of users and roles that have access to a host.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The access list for the host.
* 400:
* description: Invalid host ID.
* 403:
* description: Not host owner.
* 500:
* description: Failed to get access list.
*/
router.get(
"/host/:id/access",
authenticateJWT,
@@ -338,8 +430,20 @@ router.get(
},
);
// Get user's shared hosts (hosts shared WITH this user)
// GET /rbac/shared-hosts
/**
* @openapi
* /rbac/shared-hosts:
* get:
* summary: Get shared hosts
* description: Retrieves the list of hosts that have been shared with the authenticated user.
* tags:
* - RBAC
* responses:
* 200:
* description: A list of shared hosts.
* 500:
* description: Failed to get shared hosts.
*/
router.get(
"/shared-hosts",
authenticateJWT,
@@ -385,8 +489,20 @@ router.get(
},
);
// Get all roles
// GET /rbac/roles
/**
* @openapi
* /rbac/roles:
* get:
* summary: Get all roles
* description: Retrieves a list of all roles.
* tags:
* - RBAC
* responses:
* 200:
* description: A list of roles.
* 500:
* description: Failed to get roles.
*/
router.get(
"/roles",
authenticateJWT,
@@ -413,8 +529,20 @@ router.get(
},
);
// Get all roles
// GET /rbac/roles
/**
* @openapi
* /rbac/roles:
* get:
* summary: Get all roles
* description: Retrieves a list of all roles.
* tags:
* - RBAC
* responses:
* 200:
* description: A list of roles.
* 500:
* description: Failed to get roles.
*/
router.get(
"/roles",
authenticateJWT,
@@ -443,8 +571,37 @@ router.get(
},
);
// Create new role
// POST /rbac/roles
/**
* @openapi
* /rbac/roles:
* post:
* summary: Create a new role
* description: Creates a new role.
* tags:
* - RBAC
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* displayName:
* type: string
* description:
* type: string
* responses:
* 201:
* description: Role created successfully.
* 400:
* description: Invalid request body.
* 409:
* description: A role with this name already exists.
* 500:
* description: Failed to create role.
*/
router.post(
"/roles",
authenticateJWT,
@@ -503,8 +660,41 @@ router.post(
},
);
// Update role
// PUT /rbac/roles/:id
/**
* @openapi
* /rbac/roles/{id}:
* put:
* summary: Update a role
* description: Updates a role by its ID.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* displayName:
* type: string
* description:
* type: string
* responses:
* 200:
* description: Role updated successfully.
* 400:
* description: Invalid request body or role ID.
* 404:
* description: Role not found.
* 500:
* description: Failed to update role.
*/
router.put(
"/roles/:id",
authenticateJWT,
@@ -570,8 +760,32 @@ router.put(
},
);
// Delete role
// DELETE /rbac/roles/:id
/**
* @openapi
* /rbac/roles/{id}:
* delete:
* summary: Delete a role
* description: Deletes a role by its ID.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Role deleted successfully.
* 400:
* description: Invalid role ID.
* 403:
* description: Cannot delete system roles.
* 404:
* description: Role not found.
* 500:
* description: Failed to delete role.
*/
router.delete(
"/roles/:id",
authenticateJWT,
@@ -634,8 +848,43 @@ router.delete(
},
);
// Assign role to user
// POST /rbac/users/:userId/roles
/**
* @openapi
* /rbac/users/{userId}/roles:
* post:
* summary: Assign a role to a user
* description: Assigns a role to a user.
* tags:
* - RBAC
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* roleId:
* type: integer
* responses:
* 200:
* description: Role assigned successfully.
* 400:
* description: Role ID is required.
* 403:
* description: System roles cannot be manually assigned.
* 404:
* description: User or role not found.
* 409:
* description: Role already assigned.
* 500:
* description: Failed to assign role.
*/
router.post(
"/users/:userId/roles",
authenticateJWT,
@@ -746,8 +995,37 @@ router.post(
},
);
// Remove role from user
// DELETE /rbac/users/:userId/roles/:roleId
/**
* @openapi
* /rbac/users/{userId}/roles/{roleId}:
* delete:
* summary: Remove a role from a user
* description: Removes a role from a user.
* tags:
* - RBAC
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: string
* - in: path
* name: roleId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Role removed successfully.
* 400:
* description: Invalid role ID.
* 403:
* description: System roles cannot be removed.
* 404:
* description: Role not found.
* 500:
* description: Failed to remove role.
*/
router.delete(
"/users/:userId/roles/:roleId",
authenticateJWT,
@@ -805,8 +1083,28 @@ router.delete(
},
);
// Get user's roles
// GET /rbac/users/:userId/roles
/**
* @openapi
* /rbac/users/{userId}/roles:
* get:
* summary: Get user's roles
* description: Retrieves a list of roles for a specific user.
* tags:
* - RBAC
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: A list of roles.
* 403:
* description: Access denied.
* 500:
* description: Failed to get user roles.
*/
router.get(
"/users/:userId/roles",
authenticateJWT,

View File

@@ -17,8 +17,22 @@ const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Get all snippet folders
// GET /snippets/folders
/**
* @openapi
* /snippets/folders:
* get:
* summary: Get all snippet folders
* description: Retrieves all snippet folders for the authenticated user.
* tags:
* - Snippets
* responses:
* 200:
* description: A list of snippet folders.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch snippet folders.
*/
router.get(
"/folders",
authenticateJWT,
@@ -46,8 +60,37 @@ router.get(
},
);
// Create a new snippet folder
// POST /snippets/folders
/**
* @openapi
* /snippets/folders:
* post:
* summary: Create a new snippet folder
* description: Creates a new snippet folder for the authenticated user.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* color:
* type: string
* icon:
* type: string
* responses:
* 201:
* description: Snippet folder created successfully.
* 400:
* description: Folder name is required.
* 409:
* description: Folder with this name already exists.
* 500:
* description: Failed to create snippet folder.
*/
router.post(
"/folders",
authenticateJWT,
@@ -110,8 +153,41 @@ router.post(
},
);
// Update snippet folder metadata (color, icon)
// PUT /snippets/folders/:name/metadata
/**
* @openapi
* /snippets/folders/{name}/metadata:
* put:
* summary: Update snippet folder metadata
* description: Updates the metadata (color, icon) of a snippet folder.
* tags:
* - Snippets
* parameters:
* - in: path
* name: name
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* color:
* type: string
* icon:
* type: string
* responses:
* 200:
* description: Snippet folder metadata updated successfully.
* 400:
* description: Invalid request.
* 404:
* description: Folder not found.
* 500:
* description: Failed to update snippet folder metadata.
*/
router.put(
"/folders/:name/metadata",
authenticateJWT,
@@ -194,8 +270,37 @@ router.put(
},
);
// Rename snippet folder
// PUT /snippets/folders/rename
/**
* @openapi
* /snippets/folders/rename:
* put:
* summary: Rename a snippet folder
* description: Renames a snippet folder for the authenticated user.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* oldName:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Folder renamed successfully.
* 400:
* description: Invalid request.
* 404:
* description: Folder not found.
* 409:
* description: Folder with new name already exists.
* 500:
* description: Failed to rename snippet folder.
*/
router.put(
"/folders/rename",
authenticateJWT,
@@ -282,8 +387,28 @@ router.put(
},
);
// Delete snippet folder
// DELETE /snippets/folders/:name
/**
* @openapi
* /snippets/folders/{name}:
* delete:
* summary: Delete a snippet folder
* description: Deletes a snippet folder and moves its snippets to the root.
* tags:
* - Snippets
* parameters:
* - in: path
* name: name
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Snippet folder deleted successfully.
* 400:
* description: Invalid request.
* 500:
* description: Failed to delete snippet folder.
*/
router.delete(
"/folders/:name",
authenticateJWT,
@@ -338,8 +463,40 @@ router.delete(
},
);
// Reorder snippets (bulk update)
// PUT /snippets/reorder
/**
* @openapi
* /snippets/reorder:
* put:
* summary: Reorder snippets
* description: Bulk updates the order and folder of snippets.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* snippets:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* order:
* type: integer
* folder:
* type: string
* responses:
* 200:
* description: Snippets reordered successfully.
* 400:
* description: Invalid request.
* 500:
* description: Failed to reorder snippets.
*/
router.put(
"/reorder",
authenticateJWT,
@@ -405,8 +562,35 @@ router.put(
},
);
// Execute a snippet on a host
// POST /snippets/execute
/**
* @openapi
* /snippets/execute:
* post:
* summary: Execute a snippet on a host
* description: Executes a snippet on a specified host.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* snippetId:
* type: integer
* hostId:
* type: integer
* responses:
* 200:
* description: Snippet executed successfully.
* 400:
* description: Snippet ID and Host ID are required.
* 404:
* description: Snippet or host not found.
* 500:
* description: Failed to execute snippet.
*/
router.post(
"/execute",
authenticateJWT,
@@ -662,8 +846,22 @@ router.post(
},
);
// Get all snippets for the authenticated user
// GET /snippets
/**
* @openapi
* /snippets:
* get:
* summary: Get all snippets
* description: Retrieves all snippets for the authenticated user.
* tags:
* - Snippets
* responses:
* 200:
* description: A list of snippets.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch snippets.
*/
router.get(
"/",
authenticateJWT,
@@ -696,8 +894,30 @@ router.get(
},
);
// Get a specific snippet by ID
// GET /snippets/:id
/**
* @openapi
* /snippets/{id}:
* get:
* summary: Get a specific snippet
* description: Retrieves a specific snippet by its ID.
* tags:
* - Snippets
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The requested snippet.
* 400:
* description: Invalid request parameters.
* 404:
* description: Snippet not found.
* 500:
* description: Failed to fetch snippet.
*/
router.get(
"/:id",
authenticateJWT,
@@ -735,8 +955,39 @@ router.get(
},
);
// Create a new snippet
// POST /snippets
/**
* @openapi
* /snippets:
* post:
* summary: Create a new snippet
* description: Creates a new snippet for the authenticated user.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* content:
* type: string
* description:
* type: string
* folder:
* type: string
* order:
* type: integer
* responses:
* 201:
* description: Snippet created successfully.
* 400:
* description: Name and content are required.
* 500:
* description: Failed to create snippet.
*/
router.post(
"/",
authenticateJWT,
@@ -806,8 +1057,47 @@ router.post(
},
);
// Update a snippet
// PUT /snippets/:id
/**
* @openapi
* /snippets/{id}:
* put:
* summary: Update a snippet
* description: Updates a specific snippet by its ID.
* tags:
* - Snippets
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* content:
* type: string
* description:
* type: string
* folder:
* type: string
* order:
* type: integer
* responses:
* 200:
* description: The updated snippet.
* 400:
* description: Invalid request.
* 404:
* description: Snippet not found.
* 500:
* description: Failed to update snippet.
*/
router.put(
"/:id",
authenticateJWT,
@@ -883,8 +1173,30 @@ router.put(
},
);
// Delete a snippet
// DELETE /snippets/:id
/**
* @openapi
* /snippets/{id}:
* delete:
* summary: Delete a snippet
* description: Deletes a specific snippet by its ID.
* tags:
* - Snippets
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Snippet deleted successfully.
* 400:
* description: Invalid request.
* 404:
* description: Snippet not found.
* 500:
* description: Failed to delete snippet.
*/
router.delete(
"/:id",
authenticateJWT,

View File

@@ -53,6 +53,22 @@ const permissionManager = PermissionManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
/**
* @openapi
* /ssh/db/host/internal:
* get:
* summary: Get internal SSH host data
* description: Returns internal SSH host data for autostart tunnels. Requires internal auth token.
* tags:
* - SSH
* responses:
* 200:
* description: A list of autostart hosts.
* 403:
* description: Forbidden.
* 500:
* description: Failed to fetch autostart SSH data.
*/
router.get("/db/host/internal", async (req: Request, res: Response) => {
try {
const internalToken = req.headers["x-internal-auth-token"];
@@ -135,6 +151,22 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
}
});
/**
* @openapi
* /ssh/db/host/internal/all:
* get:
* summary: Get all internal SSH host data
* description: Returns all internal SSH host data. Requires internal auth token.
* tags:
* - SSH
* responses:
* 200:
* description: A list of all hosts.
* 401:
* description: Invalid or missing internal authentication token.
* 500:
* description: Failed to fetch all hosts.
*/
router.get("/db/host/internal/all", async (req: Request, res: Response) => {
try {
const internalToken = req.headers["x-internal-auth-token"];
@@ -194,8 +226,22 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => {
}
});
// Route: Create SSH data (requires JWT)
// POST /ssh/host
/**
* @openapi
* /ssh/db/host:
* post:
* summary: Create SSH host
* description: Creates a new SSH host configuration.
* tags:
* - SSH
* responses:
* 200:
* description: Host created successfully.
* 400:
* description: Invalid SSH data.
* 500:
* description: Failed to save SSH data.
*/
router.post(
"/db/host",
authenticateJWT,
@@ -438,8 +484,32 @@ router.post(
},
);
// Route: Update SSH data (requires JWT)
// PUT /ssh/host/:id
/**
* @openapi
* /ssh/db/host/{id}:
* put:
* summary: Update SSH host
* description: Updates an existing SSH host configuration.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Host updated successfully.
* 400:
* description: Invalid SSH data.
* 403:
* description: Access denied.
* 404:
* description: Host not found.
* 500:
* description: Failed to update SSH data.
*/
router.put(
"/db/host/:id",
authenticateJWT,
@@ -785,8 +855,22 @@ router.put(
},
);
// Route: Get SSH data for the authenticated user (requires JWT)
// GET /ssh/host
/**
* @openapi
* /ssh/db/host:
* get:
* summary: Get all SSH hosts
* description: Retrieves all SSH hosts for the authenticated user.
* tags:
* - SSH
* responses:
* 200:
* description: A list of SSH hosts.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch SSH data.
*/
router.get(
"/db/host",
authenticateJWT,
@@ -966,8 +1050,30 @@ router.get(
},
);
// Route: Get SSH host by ID (requires JWT)
// GET /ssh/host/:id
/**
* @openapi
* /ssh/db/host/{id}:
* get:
* summary: Get SSH host by ID
* description: Retrieves a specific SSH host by its ID.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The requested SSH host.
* 400:
* description: Invalid userId or hostId.
* 404:
* description: SSH host not found.
* 500:
* description: Failed to fetch SSH host.
*/
router.get(
"/db/host/:id",
authenticateJWT,
@@ -1041,8 +1147,30 @@ router.get(
},
);
// Route: Export SSH host with decrypted credentials (requires data access)
// GET /ssh/db/host/:id/export
/**
* @openapi
* /ssh/db/host/{id}/export:
* get:
* summary: Export SSH host
* description: Exports a specific SSH host with decrypted credentials.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The exported SSH host.
* 400:
* description: Invalid userId or hostId.
* 404:
* description: SSH host not found.
* 500:
* description: Failed to export SSH host.
*/
router.get(
"/db/host/:id/export",
authenticateJWT,
@@ -1121,8 +1249,30 @@ router.get(
},
);
// Route: Delete SSH host by id (requires JWT)
// DELETE /ssh/host/:id
/**
* @openapi
* /ssh/db/host/{id}:
* delete:
* summary: Delete SSH host
* description: Deletes an SSH host by its ID.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: SSH host deleted successfully.
* 400:
* description: Invalid userId or id.
* 404:
* description: SSH host not found.
* 500:
* description: Failed to delete SSH host.
*/
router.delete(
"/db/host/:id",
authenticateJWT,
@@ -1237,8 +1387,28 @@ router.delete(
},
);
// Route: Get recent files (requires JWT)
// GET /ssh/file_manager/recent
/**
* @openapi
* /ssh/file_manager/recent:
* get:
* summary: Get recent files
* description: Retrieves a list of recent files for a specific host.
* tags:
* - SSH
* parameters:
* - in: query
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of recent files.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch recent files.
*/
router.get(
"/file_manager/recent",
authenticateJWT,
@@ -1279,8 +1449,35 @@ router.get(
},
);
// Route: Add recent file (requires JWT)
// POST /ssh/file_manager/recent
/**
* @openapi
* /ssh/file_manager/recent:
* post:
* summary: Add recent file
* description: Adds a file to the list of recent files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* name:
* type: string
* responses:
* 200:
* description: Recent file added.
* 400:
* description: Invalid data.
* 500:
* description: Failed to add recent file.
*/
router.post(
"/file_manager/recent",
authenticateJWT,
@@ -1328,8 +1525,33 @@ router.post(
},
);
// Route: Remove recent file (requires JWT)
// DELETE /ssh/file_manager/recent
/**
* @openapi
* /ssh/file_manager/recent:
* delete:
* summary: Remove recent file
* description: Removes a file from the list of recent files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* responses:
* 200:
* description: Recent file removed.
* 400:
* description: Invalid data.
* 500:
* description: Failed to remove recent file.
*/
router.delete(
"/file_manager/recent",
authenticateJWT,
@@ -1361,8 +1583,28 @@ router.delete(
},
);
// Route: Get pinned files (requires JWT)
// GET /ssh/file_manager/pinned
/**
* @openapi
* /ssh/file_manager/pinned:
* get:
* summary: Get pinned files
* description: Retrieves a list of pinned files for a specific host.
* tags:
* - SSH
* parameters:
* - in: query
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of pinned files.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch pinned files.
*/
router.get(
"/file_manager/pinned",
authenticateJWT,
@@ -1402,8 +1644,37 @@ router.get(
},
);
// Route: Add pinned file (requires JWT)
// POST /ssh/file_manager/pinned
/**
* @openapi
* /ssh/file_manager/pinned:
* post:
* summary: Add pinned file
* description: Adds a file to the list of pinned files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* name:
* type: string
* responses:
* 200:
* description: File pinned.
* 400:
* description: Invalid data.
* 409:
* description: File already pinned.
* 500:
* description: Failed to pin file.
*/
router.post(
"/file_manager/pinned",
authenticateJWT,
@@ -1448,8 +1719,33 @@ router.post(
},
);
// Route: Remove pinned file (requires JWT)
// DELETE /ssh/file_manager/pinned
/**
* @openapi
* /ssh/file_manager/pinned:
* delete:
* summary: Remove pinned file
* description: Removes a file from the list of pinned files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* responses:
* 200:
* description: Pinned file removed.
* 400:
* description: Invalid data.
* 500:
* description: Failed to remove pinned file.
*/
router.delete(
"/file_manager/pinned",
authenticateJWT,
@@ -1481,8 +1777,28 @@ router.delete(
},
);
// Route: Get shortcuts (requires JWT)
// GET /ssh/file_manager/shortcuts
/**
* @openapi
* /ssh/file_manager/shortcuts:
* get:
* summary: Get shortcuts
* description: Retrieves a list of shortcuts for a specific host.
* tags:
* - SSH
* parameters:
* - in: query
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of shortcuts.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch shortcuts.
*/
router.get(
"/file_manager/shortcuts",
authenticateJWT,
@@ -1522,8 +1838,37 @@ router.get(
},
);
// Route: Add shortcut (requires JWT)
// POST /ssh/file_manager/shortcuts
/**
* @openapi
* /ssh/file_manager/shortcuts:
* post:
* summary: Add shortcut
* description: Adds a shortcut for a specific host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* name:
* type: string
* responses:
* 200:
* description: Shortcut added.
* 400:
* description: Invalid data.
* 409:
* description: Shortcut already exists.
* 500:
* description: Failed to add shortcut.
*/
router.post(
"/file_manager/shortcuts",
authenticateJWT,
@@ -1568,8 +1913,33 @@ router.post(
},
);
// Route: Remove shortcut (requires JWT)
// DELETE /ssh/file_manager/shortcuts
/**
* @openapi
* /ssh/file_manager/shortcuts:
* delete:
* summary: Remove shortcut
* description: Removes a shortcut for a specific host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* responses:
* 200:
* description: Shortcut removed.
* 400:
* description: Invalid data.
* 500:
* description: Failed to remove shortcut.
*/
router.delete(
"/file_manager/shortcuts",
authenticateJWT,
@@ -1601,8 +1971,28 @@ router.delete(
},
);
// Route: Get command history for a host
// GET /ssh/command-history/:hostId
/**
* @openapi
* /ssh/command-history/{hostId}:
* get:
* summary: Get command history
* description: Retrieves the command history for a specific host.
* tags:
* - SSH
* parameters:
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of commands.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch command history.
*/
router.get(
"/command-history/:hostId",
authenticateJWT,
@@ -1647,8 +2037,33 @@ router.get(
},
);
// Route: Delete command from history
// DELETE /ssh/command-history
/**
* @openapi
* /ssh/command-history:
* delete:
* summary: Delete command from history
* description: Deletes a specific command from the history of a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* command:
* type: string
* responses:
* 200:
* description: Command deleted from history.
* 400:
* description: Invalid data.
* 500:
* description: Failed to delete command.
*/
router.delete(
"/command-history",
authenticateJWT,
@@ -1700,9 +2115,8 @@ async function resolveHostCredentials(
if (requestingUserId && requestingUserId !== ownerId) {
try {
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
host.id as number,
@@ -1790,8 +2204,33 @@ async function resolveHostCredentials(
}
}
// Route: Rename folder (requires JWT)
// PUT /ssh/db/folders/rename
/**
* @openapi
* /ssh/folders/rename:
* put:
* summary: Rename folder
* description: Renames a folder for SSH hosts and credentials.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* oldName:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Folder renamed successfully.
* 400:
* description: Old name and new name are required.
* 500:
* description: Failed to rename folder.
*/
router.put(
"/folders/rename",
authenticateJWT,
@@ -1865,8 +2304,22 @@ router.put(
},
);
// Route: Get all folders with metadata (requires JWT)
// GET /ssh/db/folders
/**
* @openapi
* /ssh/folders:
* get:
* summary: Get all folders
* description: Retrieves all folders for the authenticated user.
* tags:
* - SSH
* responses:
* 200:
* description: A list of folders.
* 400:
* description: Invalid user ID.
* 500:
* description: Failed to fetch folders.
*/
router.get("/folders", authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -1890,8 +2343,35 @@ router.get("/folders", authenticateJWT, async (req: Request, res: Response) => {
}
});
// Route: Update folder metadata (requires JWT)
// PUT /ssh/db/folders/metadata
/**
* @openapi
* /ssh/folders/metadata:
* put:
* summary: Update folder metadata
* description: Updates the metadata (color, icon) of a folder.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* color:
* type: string
* icon:
* type: string
* responses:
* 200:
* description: Folder metadata updated successfully.
* 400:
* description: Folder name is required.
* 500:
* description: Failed to update folder metadata.
*/
router.put(
"/folders/metadata",
authenticateJWT,
@@ -1944,8 +2424,28 @@ router.put(
},
);
// Route: Delete all hosts in folder (requires JWT)
// DELETE /ssh/db/folders/:name/hosts
/**
* @openapi
* /ssh/folders/{name}/hosts:
* delete:
* summary: Delete all hosts in folder
* description: Deletes all SSH hosts within a specific folder.
* tags:
* - SSH
* parameters:
* - in: path
* name: name
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Hosts deleted successfully.
* 400:
* description: Invalid folder name.
* 500:
* description: Failed to delete hosts in folder.
*/
router.delete(
"/folders/:name/hosts",
authenticateJWT,
@@ -2063,8 +2563,31 @@ router.delete(
},
);
// Route: Bulk import SSH hosts (requires JWT)
// POST /ssh/bulk-import
/**
* @openapi
* /ssh/bulk-import:
* post:
* summary: Bulk import SSH hosts
* description: Bulk imports multiple SSH hosts.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hosts:
* type: array
* items:
* type: object
* responses:
* 200:
* description: Import completed.
* 400:
* description: Invalid request body.
*/
router.post(
"/bulk-import",
authenticateJWT,
@@ -2221,8 +2744,33 @@ router.post(
},
);
// Route: Enable autostart for SSH configuration (requires JWT)
// POST /ssh/autostart/enable
/**
* @openapi
* /ssh/autostart/enable:
* post:
* summary: Enable autostart for SSH configuration
* description: Enables autostart for a specific SSH configuration.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sshConfigId:
* type: number
* responses:
* 200:
* description: AutoStart enabled successfully.
* 400:
* description: Valid sshConfigId is required.
* 404:
* description: SSH configuration not found.
* 500:
* description: Internal server error.
*/
router.post(
"/autostart/enable",
authenticateJWT,
@@ -2375,8 +2923,31 @@ router.post(
},
);
// Route: Disable autostart for SSH configuration (requires JWT)
// DELETE /ssh/autostart/disable
/**
* @openapi
* /ssh/autostart/disable:
* delete:
* summary: Disable autostart for SSH configuration
* description: Disables autostart for a specific SSH configuration.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sshConfigId:
* type: number
* responses:
* 200:
* description: AutoStart disabled successfully.
* 400:
* description: Valid sshConfigId is required.
* 500:
* description: Internal server error.
*/
router.delete(
"/autostart/disable",
authenticateJWT,
@@ -2421,8 +2992,20 @@ router.delete(
},
);
// Route: Get autostart status for user's SSH configurations (requires JWT)
// GET /ssh/autostart/status
/**
* @openapi
* /ssh/autostart/status:
* get:
* summary: Get autostart status
* description: Retrieves the autostart status for the user's SSH configurations.
* tags:
* - SSH
* responses:
* 200:
* description: A list of autostart configurations.
* 500:
* description: Internal server error.
*/
router.get(
"/autostart/status",
authenticateJWT,

View File

@@ -17,8 +17,33 @@ const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Save command to history
// POST /terminal/command_history
/**
* @openapi
* /terminal/command_history:
* post:
* summary: Save command to history
* description: Saves a command to the command history for a specific host.
* tags:
* - Terminal
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* command:
* type: string
* responses:
* 201:
* description: Command saved successfully.
* 400:
* description: Missing required parameters.
* 500:
* description: Failed to save command.
*/
router.post(
"/command_history",
authenticateJWT,
@@ -59,8 +84,28 @@ router.post(
},
);
// Get command history for a specific host
// GET /terminal/command_history/:hostId
/**
* @openapi
* /terminal/command_history/{hostId}:
* get:
* summary: Get command history
* description: Retrieves the command history for a specific host.
* tags:
* - Terminal
* parameters:
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of commands.
* 400:
* description: Invalid request parameters.
* 500:
* description: Failed to fetch history.
*/
router.get(
"/command_history/:hostId",
authenticateJWT,
@@ -107,8 +152,33 @@ router.get(
},
);
// Delete a specific command from history
// POST /terminal/command_history/delete
/**
* @openapi
* /terminal/command_history/delete:
* post:
* summary: Delete a specific command from history
* description: Deletes a specific command from the history of a host.
* tags:
* - Terminal
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* command:
* type: string
* responses:
* 200:
* description: Command deleted successfully.
* 400:
* description: Missing required parameters.
* 500:
* description: Failed to delete command.
*/
router.post(
"/command_history/delete",
authenticateJWT,
@@ -150,8 +220,28 @@ router.post(
},
);
// Clear command history for a specific host (optional feature)
// DELETE /terminal/command_history/:hostId
/**
* @openapi
* /terminal/command_history/{hostId}:
* delete:
* summary: Clear command history
* description: Clears the entire command history for a specific host.
* tags:
* - Terminal
* parameters:
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Command history cleared successfully.
* 400:
* description: Invalid request.
* 500:
* description: Failed to clear history.
*/
router.delete(
"/command_history/:hostId",
authenticateJWT,

File diff suppressed because it is too large Load Diff

View File

@@ -365,7 +365,34 @@ app.use(express.urlencoded({ limit: "100mb", extended: true }));
const authManager = AuthManager.getInstance();
app.use(authManager.createAuthMiddleware());
// POST /docker/ssh/connect - Establish SSH session
/**
* @openapi
* /docker/ssh/connect:
* post:
* summary: Establish SSH session for Docker
* description: Establishes an SSH session to a host for Docker operations.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* responses:
* 200:
* description: SSH connection established.
* 400:
* description: Missing sessionId or hostId.
* 401:
* description: Authentication required.
* 403:
* description: Docker is not enabled for this host.
* 404:
* description: Host not found.
* 500:
* description: SSH connection failed.
*/
app.post("/docker/ssh/connect", async (req, res) => {
const {
sessionId,
@@ -929,7 +956,29 @@ app.post("/docker/ssh/connect", async (req, res) => {
}
});
// POST /docker/ssh/disconnect - Close SSH session
/**
* @openapi
* /docker/ssh/disconnect:
* post:
* summary: Disconnect SSH session
* description: Closes an active SSH session for Docker operations.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: SSH session disconnected.
* 400:
* description: Session ID is required.
*/
app.post("/docker/ssh/disconnect", async (req, res) => {
const { sessionId } = req.body;
@@ -942,7 +991,35 @@ app.post("/docker/ssh/disconnect", async (req, res) => {
res.json({ success: true, message: "SSH session disconnected" });
});
// POST /docker/ssh/connect-totp - Verify TOTP and complete connection
/**
* @openapi
* /docker/ssh/connect-totp:
* post:
* summary: Verify TOTP and complete connection
* description: Verifies the TOTP code and completes the SSH connection.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* totpCode:
* type: string
* responses:
* 200:
* description: TOTP verified, SSH connection established.
* 400:
* description: Session ID and TOTP code required.
* 401:
* description: Invalid TOTP code.
* 404:
* description: TOTP session expired.
*/
app.post("/docker/ssh/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
const userId = (req as any).userId;
@@ -1105,7 +1182,29 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
session.finish(responses);
});
// POST /docker/ssh/keepalive - Keep session alive
/**
* @openapi
* /docker/ssh/keepalive:
* post:
* summary: Keep SSH session alive
* description: Keeps an active SSH session alive.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: Session keepalive successful.
* 400:
* description: Session ID is required or session not found.
*/
app.post("/docker/ssh/keepalive", async (req, res) => {
const { sessionId } = req.body;
@@ -1133,7 +1232,26 @@ app.post("/docker/ssh/keepalive", async (req, res) => {
});
});
// GET /docker/ssh/status - Check session status
/**
* @openapi
* /docker/ssh/status:
* get:
* summary: Check SSH session status
* description: Checks the status of an active SSH session.
* tags:
* - Docker
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Session status.
* 400:
* description: Session ID is required.
*/
app.get("/docker/ssh/status", async (req, res) => {
const sessionId = req.query.sessionId as string;
@@ -1146,7 +1264,28 @@ app.get("/docker/ssh/status", async (req, res) => {
res.json({ success: true, connected: isConnected });
});
// GET /docker/validate/:sessionId - Validate Docker availability
/**
* @openapi
* /docker/validate/{sessionId}:
* get:
* summary: Validate Docker availability
* description: Validates if Docker is available on the host.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Docker availability status.
* 400:
* description: SSH session not found or not connected.
* 500:
* description: Validation failed.
*/
app.get("/docker/validate/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const userId = (req as any).userId;
@@ -1236,7 +1375,32 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
}
});
// GET /docker/containers/:sessionId - List all containers
/**
* @openapi
* /docker/containers/{sessionId}:
* get:
* summary: List all containers
* description: Lists all Docker containers on the host.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: all
* schema:
* type: boolean
* responses:
* 200:
* description: A list of containers.
* 400:
* description: SSH session not found or not connected.
* 500:
* description: Failed to list containers.
*/
app.get("/docker/containers/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const all = req.query.all !== "false";
@@ -1297,7 +1461,35 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
}
});
// GET /docker/containers/:sessionId/:containerId - Get container details
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}:
* get:
* summary: Get container details
* description: Retrieves detailed information about a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container details.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to get container details.
*/
app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
@@ -1356,7 +1548,35 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
}
});
// POST /docker/containers/:sessionId/:containerId/start - Start container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/start:
* post:
* summary: Start container
* description: Starts a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container started successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to start container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/start",
async (req, res) => {
@@ -1414,7 +1634,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/stop - Stop container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/stop:
* post:
* summary: Stop container
* description: Stops a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container stopped successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to stop container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/stop",
async (req, res) => {
@@ -1472,7 +1720,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/restart - Restart container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/restart:
* post:
* summary: Restart container
* description: Restarts a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container restarted successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to restart container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/restart",
async (req, res) => {
@@ -1530,7 +1806,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/pause - Pause container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/pause:
* post:
* summary: Pause container
* description: Pauses a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container paused successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to pause container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/pause",
async (req, res) => {
@@ -1588,7 +1892,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/unpause - Unpause container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/unpause:
* post:
* summary: Unpause container
* description: Unpauses a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container unpaused successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to unpause container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/unpause",
async (req, res) => {
@@ -1646,7 +1978,39 @@ app.post(
},
);
// DELETE /docker/containers/:sessionId/:containerId/remove - Remove container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/remove:
* delete:
* summary: Remove container
* description: Removes a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* - in: query
* name: force
* schema:
* type: boolean
* responses:
* 200:
* description: Container removed successfully.
* 400:
* description: SSH session not found or not connected, or cannot remove a running container.
* 404:
* description: Container not found.
* 500:
* description: Failed to remove container.
*/
app.delete(
"/docker/containers/:sessionId/:containerId/remove",
async (req, res) => {
@@ -1718,7 +2082,51 @@ app.delete(
},
);
// GET /docker/containers/:sessionId/:containerId/logs - Get container logs
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/logs:
* get:
* summary: Get container logs
* description: Retrieves logs for a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* - in: query
* name: tail
* schema:
* type: integer
* - in: query
* name: timestamps
* schema:
* type: boolean
* - in: query
* name: since
* schema:
* type: string
* - in: query
* name: until
* schema:
* type: string
* responses:
* 200:
* description: Container logs.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to get container logs.
*/
app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
const { sessionId, containerId } = req.params;
const tail = req.query.tail ? parseInt(req.query.tail as string) : 100;
@@ -1795,7 +2203,35 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
}
});
// GET /docker/containers/:sessionId/:containerId/stats - Get container stats
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/stats:
* get:
* summary: Get container stats
* description: Retrieves stats for a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container stats.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to get container stats.
*/
app.get(
"/docker/containers/:sessionId/:containerId/stats",
async (req, res) => {

View File

@@ -416,6 +416,24 @@ function detectBinary(buffer: Buffer): boolean {
return nullBytes / sampleSize > 0.01;
}
/**
* @openapi
* /ssh/file_manager/ssh/connect:
* post:
* summary: Connect to SSH for file manager
* description: Establishes an SSH connection for file manager operations.
* tags:
* - File Manager
* responses:
* 200:
* description: SSH connection established.
* 400:
* description: Missing SSH connection parameters.
* 401:
* description: Authentication required.
* 500:
* description: SSH connection failed.
*/
app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
const {
sessionId,
@@ -986,6 +1004,26 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
}
});
/**
* @openapi
* /ssh/file_manager/ssh/connect-totp:
* post:
* summary: Verify TOTP and complete connection
* description: Verifies the TOTP code and completes the SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: TOTP verified, SSH connection established.
* 400:
* description: Session ID and TOTP code required.
* 401:
* description: Invalid TOTP code or authentication required.
* 404:
* description: TOTP session expired.
* 408:
* description: TOTP session timeout.
*/
app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
@@ -1149,18 +1187,71 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
session.finish(responses);
});
/**
* @openapi
* /ssh/file_manager/ssh/disconnect:
* post:
* summary: Disconnect from SSH
* description: Closes an active SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: SSH connection disconnected.
*/
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
const { sessionId } = req.body;
cleanupSession(sessionId);
res.json({ status: "success", message: "SSH connection disconnected" });
});
/**
* @openapi
* /ssh/file_manager/ssh/status:
* get:
* summary: Get SSH connection status
* description: Checks the status of an SSH connection for file manager.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: SSH connection status.
*/
app.get("/ssh/file_manager/ssh/status", (req, res) => {
const sessionId = req.query.sessionId as string;
const isConnected = !!sshSessions[sessionId]?.isConnected;
res.json({ status: "success", connected: isConnected });
});
/**
* @openapi
* /ssh/file_manager/ssh/keepalive:
* post:
* summary: Keep SSH session alive
* description: Keeps an active SSH session for file manager alive.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: Session keepalive successful.
* 400:
* description: Session ID is required or session not found.
*/
app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
const { sessionId } = req.body;
@@ -1188,6 +1279,33 @@ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/listFiles:
* get:
* summary: List files in a directory
* description: Lists the files and directories in a given path on the remote host.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: A list of files and directories.
* 400:
* description: Session ID is required or SSH connection not established.
* 500:
* description: Failed to list files.
*/
app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
@@ -1387,6 +1505,33 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
trySFTP();
});
/**
* @openapi
* /ssh/file_manager/ssh/identifySymlink:
* get:
* summary: Identify symbolic link
* description: Identifies the target of a symbolic link.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Symbolic link information.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to identify symbolic link.
*/
app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
@@ -1454,6 +1599,35 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/readFile:
* get:
* summary: Read a file
* description: Reads the content of a file from the remote host.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The content of the file.
* 400:
* description: Missing required parameters or file too large.
* 404:
* description: File not found.
* 500:
* description: Failed to read file.
*/
app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
@@ -1592,6 +1766,35 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
);
});
/**
* @openapi
* /ssh/file_manager/ssh/writeFile:
* post:
* summary: Write to a file
* description: Writes content to a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* content:
* type: string
* responses:
* 200:
* description: File written successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to write file.
*/
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const { sessionId, path: filePath, content } = req.body;
const sshConn = sshSessions[sessionId];
@@ -1737,10 +1940,15 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
if (err) {
fileLogger.error("Fallback write command failed:", err);
if (!res.headersSent) {
return res.status(500).json({
error: `Write failed: ${err.message}`,
toast: { type: "error", message: `Write failed: ${err.message}` },
});
return res
.status(500)
.json({
error: `Write failed: ${err.message}`,
toast: {
type: "error",
message: `Write failed: ${err.message}`,
},
});
}
return;
}
@@ -1803,6 +2011,37 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
trySFTP();
});
/**
* @openapi
* /ssh/file_manager/ssh/uploadFile:
* post:
* summary: Upload a file
* description: Uploads a file to the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* content:
* type: string
* fileName:
* type: string
* responses:
* 200:
* description: File uploaded successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to upload file.
*/
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const { sessionId, path: filePath, content, fileName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2103,6 +2342,37 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
trySFTP();
});
/**
* @openapi
* /ssh/file_manager/ssh/createFile:
* post:
* summary: Create a file
* description: Creates an empty file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* fileName:
* type: string
* responses:
* 200:
* description: File created successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to create file.
*/
app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
const { sessionId, path: filePath, fileName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2204,6 +2474,37 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/createFolder:
* post:
* summary: Create a folder
* description: Creates a new folder on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* folderName:
* type: string
* responses:
* 200:
* description: Folder created successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to create folder.
*/
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
const { sessionId, path: folderPath, folderName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2305,6 +2606,37 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/deleteItem:
* delete:
* summary: Delete a file or directory
* description: Deletes a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* isDirectory:
* type: boolean
* responses:
* 200:
* description: Item deleted successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to delete item.
*/
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
const { sessionId, path: itemPath, isDirectory } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2407,6 +2739,37 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/renameItem:
* put:
* summary: Rename a file or directory
* description: Renames a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* oldPath:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Item renamed successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to rename item.
*/
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
const { sessionId, oldPath, newName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2515,6 +2878,39 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/moveItem:
* put:
* summary: Move a file or directory
* description: Moves a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* oldPath:
* type: string
* newPath:
* type: string
* responses:
* 200:
* description: Item moved successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 408:
* description: Move operation timed out.
* 500:
* description: Failed to move item.
*/
app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
const { sessionId, oldPath, newPath } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2640,6 +3036,37 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/downloadFile:
* post:
* summary: Download a file
* description: Downloads a file from the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* hostId:
* type: integer
* userId:
* type: string
* responses:
* 200:
* description: The file content.
* 400:
* description: Missing required parameters or file too large.
* 500:
* description: Failed to download file.
*/
app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
const { sessionId, path: filePath, hostId, userId } = req.body;
@@ -2741,6 +3168,39 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/copyItem:
* post:
* summary: Copy a file or directory
* description: Copies a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* sourcePath:
* type: string
* targetDir:
* type: string
* hostId:
* type: integer
* userId:
* type: string
* responses:
* 200:
* description: Item copied successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to copy item.
*/
app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
const { sessionId, sourcePath, targetDir, hostId, userId } = req.body;
@@ -2904,6 +3364,33 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/executeFile:
* post:
* summary: Execute a file
* description: Executes a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* filePath:
* type: string
* responses:
* 200:
* description: File execution result.
* 400:
* description: Missing required parameters or SSH connection not available.
* 500:
* description: Failed to execute file.
*/
app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
const { sessionId, filePath } = req.body;
const sshConn = sshSessions[sessionId];
@@ -3002,6 +3489,37 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/changePermissions:
* post:
* summary: Change file permissions
* description: Changes the permissions of a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* permissions:
* type: string
* responses:
* 200:
* description: Permissions changed successfully.
* 400:
* description: Missing required parameters or SSH connection not available.
* 408:
* description: Permission change timed out.
* 500:
* description: Failed to change permissions.
*/
app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => {
const { sessionId, path, permissions } = req.body;
const sshConn = sshSessions[sessionId];
@@ -3319,8 +3837,39 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => {
});
});
// Route: Compress files/folders (requires JWT)
// POST /ssh/file_manager/ssh/compressFiles
/**
* @openapi
* /ssh/file_manager/ssh/compressFiles:
* post:
* summary: Compress files
* description: Compresses files and/or directories on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* paths:
* type: array
* items:
* type: string
* archiveName:
* type: string
* format:
* type: string
* responses:
* 200:
* description: Files compressed successfully.
* 400:
* description: Missing required parameters or unsupported compression format.
* 500:
* description: Failed to compress files.
*/
app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => {
const { sessionId, paths, archiveName, format } = req.body;

View File

@@ -1924,6 +1924,20 @@ function tcpPing(
});
}
/**
* @openapi
* /status:
* get:
* summary: Get all host statuses
* description: Retrieves the status of all hosts for the authenticated user.
* tags:
* - Server Stats
* responses:
* 200:
* description: A map of host IDs to their status entries.
* 401:
* description: Session expired - please log in again.
*/
app.get("/status", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -1946,6 +1960,28 @@ app.get("/status", async (req, res) => {
res.json(result);
});
/**
* @openapi
* /status/{id}:
* get:
* summary: Get host status by ID
* description: Retrieves the status of a specific host by its ID.
* tags:
* - Server Stats
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Host status entry.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Status not available.
*/
app.get("/status/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -1970,6 +2006,20 @@ app.get("/status/:id", validateHostId, async (req, res) => {
res.json(statusEntry);
});
/**
* @openapi
* /clear-connections:
* post:
* summary: Clear all SSH connections
* description: Clears all SSH connections from the connection pool.
* tags:
* - Server Stats
* responses:
* 200:
* description: All SSH connections cleared.
* 401:
* description: Session expired - please log in again.
*/
app.post("/clear-connections", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -1984,6 +2034,20 @@ app.post("/clear-connections", async (req, res) => {
res.json({ message: "All SSH connections cleared" });
});
/**
* @openapi
* /refresh:
* post:
* summary: Refresh polling
* description: Clears all SSH connections and refreshes host polling.
* tags:
* - Server Stats
* responses:
* 200:
* description: Polling refreshed.
* 401:
* description: Session expired - please log in again.
*/
app.post("/refresh", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -2000,6 +2064,35 @@ app.post("/refresh", async (req, res) => {
res.json({ message: "Polling refreshed" });
});
/**
* @openapi
* /host-updated:
* post:
* summary: Start polling for updated host
* description: Starts polling for a specific host after it has been updated.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* responses:
* 200:
* description: Host polling started.
* 400:
* description: Invalid hostId.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Host not found.
* 500:
* description: Failed to start polling.
*/
app.post("/host-updated", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const { hostId } = req.body;
@@ -2035,6 +2128,33 @@ app.post("/host-updated", async (req, res) => {
}
});
/**
* @openapi
* /host-deleted:
* post:
* summary: Stop polling for deleted host
* description: Stops polling for a specific host after it has been deleted.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* responses:
* 200:
* description: Host polling stopped.
* 400:
* description: Invalid hostId.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to stop polling.
*/
app.post("/host-deleted", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const { hostId } = req.body;
@@ -2063,6 +2183,28 @@ app.post("/host-deleted", async (req, res) => {
}
});
/**
* @openapi
* /metrics/{id}:
* get:
* summary: Get host metrics
* description: Retrieves current metrics for a specific host including CPU, memory, disk, network, processes, and system information.
* tags:
* - Server Stats
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Host metrics data.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Metrics not available.
*/
app.get("/metrics/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -2100,6 +2242,30 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
});
});
/**
* @openapi
* /metrics/start/{id}:
* post:
* summary: Start metrics collection
* description: Establishes an SSH connection and starts collecting metrics for a specific host.
* tags:
* - Server Stats
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Metrics collection started successfully, or TOTP required.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Host not found.
* 500:
* description: Failed to start metrics collection.
*/
app.post("/metrics/start/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -2279,6 +2445,37 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
}
});
/**
* @openapi
* /metrics/stop/{id}:
* post:
* summary: Stop metrics collection
* description: Stops metrics collection for a specific host and cleans up the SSH session.
* tags:
* - Server 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: Metrics collection stopped successfully.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to stop metrics collection.
*/
app.post("/metrics/stop/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -2321,6 +2518,37 @@ app.post("/metrics/stop/:id", validateHostId, async (req, res) => {
}
});
/**
* @openapi
* /metrics/connect-totp:
* post:
* summary: Complete TOTP verification for metrics
* description: Verifies the TOTP code and completes the metrics SSH connection.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* totpCode:
* type: string
* responses:
* 200:
* description: TOTP verified, metrics connection established.
* 400:
* description: Missing sessionId or totpCode.
* 401:
* description: Session expired or invalid TOTP code.
* 404:
* description: TOTP session not found or expired.
* 500:
* description: Failed to verify TOTP.
*/
app.post("/metrics/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -2456,6 +2684,35 @@ app.post("/metrics/connect-totp", async (req, res) => {
}
});
/**
* @openapi
* /metrics/heartbeat:
* post:
* summary: Update viewer heartbeat
* description: Updates the heartbeat timestamp for a metrics viewer session to keep it alive.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* viewerSessionId:
* type: string
* responses:
* 200:
* description: Heartbeat updated successfully.
* 400:
* description: Invalid viewerSessionId.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Viewer session not found.
* 500:
* description: Failed to update heartbeat.
*/
app.post("/metrics/heartbeat", async (req, res) => {
const { viewerSessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -2488,6 +2745,33 @@ app.post("/metrics/heartbeat", async (req, res) => {
}
});
/**
* @openapi
* /metrics/register-viewer:
* post:
* summary: Register metrics viewer
* description: Registers a new viewer session for a host to track who is viewing metrics.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* responses:
* 200:
* description: Viewer registered successfully.
* 400:
* description: Invalid hostId.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to register viewer.
*/
app.post("/metrics/register-viewer", async (req, res) => {
const { hostId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -2518,6 +2802,35 @@ app.post("/metrics/register-viewer", async (req, res) => {
}
});
/**
* @openapi
* /metrics/unregister-viewer:
* post:
* summary: Unregister metrics viewer
* description: Unregisters a viewer session when they stop viewing metrics for a host.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* viewerSessionId:
* type: string
* responses:
* 200:
* description: Viewer unregistered successfully.
* 400:
* description: Invalid hostId or viewerSessionId.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to unregister viewer.
*/
app.post("/metrics/unregister-viewer", async (req, res) => {
const { hostId, viewerSessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;

View File

@@ -1450,10 +1450,42 @@ async function killRemoteTunnelByMarker(
}
}
/**
* @openapi
* /ssh/tunnel/status:
* get:
* summary: Get all tunnel statuses
* description: Retrieves the status of all SSH tunnels.
* tags:
* - SSH Tunnels
* responses:
* 200:
* description: A list of all tunnel statuses.
*/
app.get("/ssh/tunnel/status", (req, res) => {
res.json(getAllTunnelStatus());
});
/**
* @openapi
* /ssh/tunnel/status/{tunnelName}:
* get:
* summary: Get tunnel status by name
* description: Retrieves the status of a specific SSH tunnel by its name.
* tags:
* - SSH Tunnels
* parameters:
* - in: path
* name: tunnelName
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Tunnel status.
* 404:
* description: Tunnel not found.
*/
app.get("/ssh/tunnel/status/:tunnelName", (req, res) => {
const { tunnelName } = req.params;
const status = connectionStatus.get(tunnelName);
@@ -1465,6 +1497,39 @@ app.get("/ssh/tunnel/status/:tunnelName", (req, res) => {
res.json({ name: tunnelName, status });
});
/**
* @openapi
* /ssh/tunnel/connect:
* post:
* summary: Connect SSH tunnel
* description: Establishes an SSH tunnel connection with the specified configuration.
* tags:
* - SSH Tunnels
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* sourceHostId:
* type: integer
* tunnelIndex:
* type: integer
* responses:
* 200:
* description: Connection request received.
* 400:
* description: Invalid tunnel configuration.
* 401:
* description: Authentication required.
* 403:
* description: Access denied to this host.
* 500:
* description: Failed to connect tunnel.
*/
app.post(
"/ssh/tunnel/connect",
authenticateJWT,
@@ -1619,6 +1684,35 @@ app.post(
},
);
/**
* @openapi
* /ssh/tunnel/disconnect:
* post:
* summary: Disconnect SSH tunnel
* description: Disconnects an active SSH tunnel.
* tags:
* - SSH Tunnels
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* tunnelName:
* type: string
* responses:
* 200:
* description: Disconnect request received.
* 400:
* description: Tunnel name required.
* 401:
* description: Authentication required.
* 403:
* description: Access denied.
* 500:
* description: Failed to disconnect tunnel.
*/
app.post(
"/ssh/tunnel/disconnect",
authenticateJWT,
@@ -1683,6 +1777,35 @@ app.post(
},
);
/**
* @openapi
* /ssh/tunnel/cancel:
* post:
* summary: Cancel tunnel retry
* description: Cancels the retry mechanism for a failed SSH tunnel connection.
* tags:
* - SSH Tunnels
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* tunnelName:
* type: string
* responses:
* 200:
* description: Cancel request received.
* 400:
* description: Tunnel name required.
* 401:
* description: Authentication required.
* 403:
* description: Access denied.
* 500:
* description: Failed to cancel tunnel retry.
*/
app.post(
"/ssh/tunnel/cancel",
authenticateJWT,

145
src/backend/swagger.ts Normal file
View File

@@ -0,0 +1,145 @@
import swaggerJSDoc from "swagger-jsdoc";
import path from "path";
import { fileURLToPath } from "url";
import { promises as fs } from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, "..", "..", "..");
const swaggerOptions: swaggerJSDoc.Options = {
definition: {
openapi: "3.0.3",
info: {
title: "Termix API",
version: "0.0.0",
description: "Termix Backend API Reference",
},
servers: [
{
url: "http://localhost:30001",
description: "Main database and authentication server",
},
{
url: "http://localhost:30003",
description: "SSH tunnel management server",
},
{
url: "http://localhost:30004",
description: "SSH file manager server",
},
{
url: "http://localhost:30005",
description: "Server statistics and monitoring server",
},
{
url: "http://localhost:30006",
description: "Dashboard server",
},
{
url: "http://localhost:30007",
description: "Docker management server",
},
],
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
schemas: {
Error: {
type: "object",
properties: {
error: { type: "string" },
details: { type: "string" },
},
},
},
},
security: [
{
bearerAuth: [],
},
],
tags: [
{
name: "Alerts",
description: "System alerts and notifications management",
},
{
name: "Credentials",
description: "SSH credential management",
},
{
name: "Network Topology",
description: "Network topology visualization and management",
},
{
name: "RBAC",
description: "Role-based access control for host sharing",
},
{
name: "Snippets",
description: "Command snippet management",
},
{
name: "Terminal",
description: "Terminal command history",
},
{
name: "Users",
description: "User management and authentication",
},
{
name: "Dashboard",
description: "Dashboard statistics and activity",
},
{
name: "Docker",
description: "Docker container management",
},
{
name: "SSH Tunnels",
description: "SSH tunnel connection management",
},
{
name: "Server Stats",
description: "Server status monitoring and metrics collection",
},
{
name: "File Manager",
description: "SSH file management operations",
},
],
},
apis: [
path.join(projectRoot, "src", "backend", "database", "routes", "*.ts"),
path.join(projectRoot, "src", "backend", "dashboard.ts"),
path.join(projectRoot, "src", "backend", "ssh", "*.ts"),
],
};
async function generateOpenAPISpec() {
try {
const swaggerSpec = swaggerJSDoc(swaggerOptions);
const outputPath = path.join(projectRoot, "openapi.json");
await fs.writeFile(
outputPath,
JSON.stringify(swaggerSpec, null, 2),
"utf-8",
);
} catch (error) {
console.error("Failed to generate OpenAPI specification:", error);
process.exit(1);
}
}
generateOpenAPISpec();
export { swaggerOptions, generateOpenAPISpec };

View File

@@ -32,7 +32,6 @@ class FieldCrypto {
"key",
"key_password",
"keyPassword",
"keyType",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
@@ -46,7 +45,6 @@ class FieldCrypto {
"key",
"public_key",
"publicKey",
"keyType",
]),
};

View File

@@ -194,7 +194,7 @@ class UserDataImport {
continue;
}
const newHostData: Record<string, unknown> = {
const newHostData: any = {
...host,
userId: targetUserId,
updatedAt: new Date().toISOString(),
@@ -204,7 +204,7 @@ class UserDataImport {
newHostData.createdAt = new Date().toISOString();
}
let processedHostData: Record<string, unknown> = newHostData;
let processedHostData: any = newHostData;
if (options.userDataKey) {
processedHostData = DataCrypto.encryptRecord(
"ssh_data",
@@ -275,7 +275,7 @@ class UserDataImport {
continue;
}
const newCredentialData: Record<string, unknown> = {
const newCredentialData: any = {
...credential,
userId: targetUserId,
updatedAt: new Date().toISOString(),
@@ -287,7 +287,7 @@ class UserDataImport {
newCredentialData.createdAt = new Date().toISOString();
}
let processedCredentialData: Record<string, unknown> = newCredentialData;
let processedCredentialData: any = newCredentialData;
if (options.userDataKey) {
processedCredentialData = DataCrypto.encryptRecord(
"ssh_credentials",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,8 @@ export type WidgetType =
| "processes"
| "system"
| "login_stats"
| "ports";
| "ports"
| "firewall";
export interface ListeningPort {
protocol: "tcp" | "udp";

View File

@@ -1,4 +1,4 @@
import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager.tsx";
import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager";
import React from "react";
const HostManagerApp: React.FC = () => {